如何用pytest编写正确的测试? [英] How to write correct test with pytest?

查看:62
本文介绍了如何用pytest编写正确的测试?的处理方法,对大家解决问题具有一定的参考价值,需要的朋友们下面随着小编来一起学习吧!

问题描述

我可以编写一些单元测试,但不知道如何编写有关将其他功能连接在一起的 createAccount()的测试.

I can write some unittests but have no idea how to write test about createAccount() which connect other functions together.

createAccount()按顺序包含一些步骤:

createAccount() contains some steps in order:

  1. 验证电子邮件

  1. Validate Email

验证密码

检查密码是否匹配

实例化新帐户对象

每一步都有一些测试用例. 因此,我的问题是: 1.如何编写 createAccount()测试用例?我应该列出所有可能的组合测试用例然后进行测试.

Every step has some test cases. So, my questions are: 1. How to write createAccount() test case ? Should I list all possible combination test cases then test them.

例如:

TestCase0.电子邮件无效

TestCase0. Email is invalid

TestCase1.重试电子邮件3次后,应用程序停止

TestCase1. App stops after retrying email 3 times

TestCase2.电子邮件可以,密码无效

TestCase2. Email is ok, password is not valid

TestCase3.电子邮件可以,密码有效,第二个密码与第一个密码不符

TestCase3. Email is ok, password is valid, 2nd password doesnt match the first one

TestCase4.电子邮件可以,密码有效,两个密码均匹配,安全性有效

TestCase4. Email is ok, password is valid, both password match, security is valid

TestCase5.电子邮件可以,密码有效,两个密码均匹配,安全性有效,帐户已成功创建

TestCase5. Email is ok, password is vailid, both password match, security is valid, account was create succesfully

  1. 我不知道如何测试,因为我的createAccount()很烂吗?如果是,如何重构它以便于测试?

这是我的代码:

class RegisterUI:

    def getEmail(self):
        return input("Please type an your email:")

    def getPassword1(self):
        return input("Please type a password:")

    def getPassword2(self):
        return input("Please confirm your password:")

    def getSecKey(self):
        return input("Please type your security keyword:")

    def printMessage(self,message):
        print(message)


class RegisterController:
    def __init__(self, view):
        self.view = view


    def displaymessage(self, message):
        self.view.printMessage(message)

    def ValidateEmail(self, email):
        """get email from user, check email
        """
        self.email = email
        email_obj = Email(self.email)
        status = email_obj.isValidEmail() and not accounts.isDuplicate(self.email)
        if not status:
            raise EmailNotOK("Email is duplicate or incorrect format")
        else:
            return True


    def ValidatePassword(self, password):
        """
        get password from user, check pass valid
        """
        self.password = password
        status = Password.isValidPassword(self.password)
        if not status:
            raise PassNotValid("Pass isn't valid")
        else: return True

    def CheckPasswordMatch(self, password):
        """
        get password 2 from user, check pass match
        """
        password_2 = password
        status = Password.isMatch(self.password, password_2)
        if not status:
            raise PassNotMatch("Pass doesn't match")
        else: return True

    def createAccount(self):
        retry = 0
        while 1:
            try:
                email_input = self.view.getEmail()
                self.ValidateEmail(email_input) #
                break
            except EmailNotOK as e:
                retry = retry + 1
                self.displaymessage(str(e))
                if retry > 3:
                    return

        while 1:
            try:
                password1_input = self.view.getPassword1()
                self.ValidatePassword(password1_input)
                break
            except PassNotValid as e:
                self.displaymessage(str(e))

        while 1:
            try:
                password2_input = self.view.getPassword2()
                self.CheckPasswordMatch(password2_input)
                break
            except PassNotMatch as e:
                self.displaymessage(str(e))

        self.seckey = self.view.getSecKey()
        account = Account(Email(self.email), Password(self.password), self.seckey)
        message = "Account was create successfully"
        self.displaymessage(message)
        return account

class Register(Option):
    def execute(self):

        view = RegisterUI()
        controller_one = RegisterController(view)
        controller_one.createAccount()




"""========================Code End=============================="""

"""Testing"""
@pytest.fixture(scope="session")
def ctrl():
    view = RegisterUI()
    return RegisterController(view)

def test_canThrowErrorEmailNotValid(ctrl):
    email = 'dddddd'
    with pytest.raises(EmailNotOK) as e:
        ctrl.ValidateEmail(email)
    assert str(e.value) == 'Email is duplicate or incorrect format'

def test_EmailIsValid(ctrl):
    email = 'hello@gmail.com'
    assert ctrl.ValidateEmail(email) == True

def test_canThrowErrorPassNotValid(ctrl):
    password = '123'
    with pytest.raises(PassNotValid) as e:
        ctrl.ValidatePassword(password)
    assert str(e.value) == "Pass isn't valid"

def test_PasswordValid(ctrl):
    password = '1234567'
    assert ctrl.ValidatePassword(password) == True

def test_canThrowErrorPassNotMatch(ctrl):
    password1=  '1234567'
    ctrl.password = password1
    password2 = 'abcdf'
    with pytest.raises(PassNotMatch) as e:
        ctrl.CheckPasswordMatch(password2)
    assert str(e.value) == "Pass doesn't match"

def test_PasswordMatch(ctrl):
    password1=  '1234567'
    ctrl.password = password1
    password2 = '1234567'
    assert ctrl.CheckPasswordMatch(password2)

推荐答案

注意:我不太了解Python,但是我知道测试.我的Python可能并不完全正确,但是技术是正确的.

Note: I don't know Python well, but I do know testing. My Python might not be entirely correct, but the techniques are.

答案在于您对createAccount的描述.它做很多事情.它具有各种验证方法的包装.它显示消息.它创建一个帐户.需要对其进行重构以进行测试.测试和重构齐头并进.

The answer lies in your description of createAccount. It does too many things. It has wrappers around various validation methods. It displays messages. It creates an account. It needs to be refactored to be testable. Testing and refactoring go hand in hand.

首先,对这四个部分分别执行提取方法重构方法.我将只执行三个验证步骤之一,它们基本上是相同的.由于这是死记硬背的操作,因此我们可以安全地执行此操作. 您的IDE甚至可以为您执行重构.

First, perform an Extract Method refactoring on each of the four pieces to turn them into their own methods. I'm only going to do one of the three validation steps, they're all basically the same. Since this is a rote operation we can do it safely. Your IDE might even be able to do the refactor for you.

def tryValidatePassword(self):
    while 1:
        try:
            password1_input = self.view.getPassword1()
            self.ValidatePassword(password1_input)
            break
        except PassNotValid as e:
            self.displaymessage(str(e))

def makeAccount(self):
    return Account(Email(self.email), Password(self.password), self.seckey)

def createAccount(self):
    self.tryValidatePassword()

    self.seckey = self.view.getSecKey()
    account = self.makeAccount()
    message = "Account was create successfully"
    self.displaymessage(message)
    return account    

仅看这段代码就会发现一个错误:createAccount如果密码错误,不会停止.

Just looking at this code reveals a bug: createAccount doesn't stop if the password is wrong.

现在,我们可以单独查看tryValidatePassword并进行测试,如果密码无效,我们将看到它进入无限循环.不好我不确定循环的目的是什么,所以让我们将其删除.

Now that we can look at tryValidatePassword alone, and test it, we see it will enter an infinite loop if the password is invalid. That's no good. I'm not sure what the purpose of the loop is, so let's remove it.

    def tryValidatePassword(self):
        try:
            password1_input = self.view.getPassword1()
            self.ValidatePassword(password1_input)
        except PassNotValid as e:
            self.displaymessage(str(e))

现在,它只是打印异常的ValidatePassword周围的包装.这揭示了几种反模式.

Now it's just a wrapper around ValidatePassword that prints the exception. This reveals several anti-patterns.

首先,ValidatePassword等正在将异常用于控制流.验证方法发现事物无效并不罕见.他们应该返回一个简单的布尔值.这简化了事情.

First, ValidatePassword, and others, are using exception for control flow. It's not exceptional for a validation method to find the thing is invalid. They should return a simple boolean. This simplifies things.

    def ValidatePassword(self, password):
        """
        get password from user, check pass valid
        """
        self.password = password
        return Password.isValidPassword(self.password)

现在,我们看到ValidatePassword正在执行两项无关的操作:设置密码并对其进行验证.设置密码应该在其他地方进行.

Now we see ValidatePassword is doing two unrelated things: setting the password and validating it. Setting the password should be happening somewhere else.

此外doc字符串不正确,它没有从用户那里获得密码,它只是对其进行检查.删除它.从其签名可以明显看出该方法的作用,ValidatePassword验证您传入的密码.

Also the doc string is incorrect, it doesn't get the password from the user, it just checks it. Delete it. What the method does is obvious from its signature, ValidatePassword validates the password you pass in.

    def ValidatePassword(self, password):
        return Password.isValidPassword(self.password)

另一个反模式是验证方法确定了控制器显示的消息.控制器(或可能的视图)应该控制消息.

Another anti-pattern is the message displayed by the controller was being determined by the validation method. The controller (or possibly view) should be controlling the message.

    def tryValidatePassword(self):
        password1_input = self.view.getPassword1()
        if !self.ValidatePassword(password1_input):
            self.displaymessage("Pass isn't valid")

最后,不是从密码中传递密码,而是从对象获取密码.这是一个副作用.这意味着您不能仅通过查看方法的参数就知道方法的所有输入.这使得很难理解该方法.

Finally, instead of passing in the password we're getting it from the object. This is a side-effect. It means you can't tell all the method's inputs just by looking at its parameters. This makes it harder to understand the method.

有时在对象上引用值是必要且方便的.但是这种方法做一件事:它验证密码.所以我们应该输入该密码.

Sometimes referencing values on the object is necessary and convenient. But this method does one thing: it validates a password. So we should pass that password in.

    def tryValidatePassword(self, password):
        if !self.ValidatePassword(password):
            self.displaymessage("Pass isn't valid")

    self.tryValidatePassword(self.view.getPassword1())


几乎没有什么可以测试!通过此操作,我们了解了实际情况,让我们将所有内容重新组合在一起. createAccount真正在做什么?


There's barely anything left to test! With that we've learned about what's really going on, let's bring it all back together. What is createAccount really doing?

  1. self.view获取内容并将其设置在self上.
  2. 验证那些东西.
  3. 如果它们无效则显示一条消息.
  4. 创建帐户.
  5. 显示成功消息.
  1. Getting things from self.view and setting them on self.
  2. Validating those things.
  3. Displaying a message if they're invalid.
  4. Creating an account.
  5. Displaying a success message.

1似乎是不必要的,为什么将字段从视图复制到控制器?在其他任何地方都从未引用过它们.现在我们将值传递给方法了,这不再是必需的.

1 seems unnecessary, why copy the fields from the view to controller? They're never referenced anywhere else. Now that we're passing values into methods this is no longer necessary.

2已经具有验证功能.现在一切都变瘦了,我们可以编写薄包装器来隐藏验证的实现.

2 already has validation functions. Now that everything is slimmed down we can write thin wrappers to hide the implementation of the validation.

4,创建帐户,我们已经分开了.

4, creating the account, we've already separated out.

3和5,显示消息,应该与工作分开.

3 and 5, displaying messages, should be separate from doing the work.

这是现在的样子.

class RegisterController:
    # Thin wrappers to hide the details of the validation implementations.
    def ValidatePassword(self, password):
        return Password.isValidPassword(password)

    # If there needs to be retries, they would happen in here.
    def ValidateEmail(self, email_string):
        email = Email(email_string)
        return email.isValidEmail() and not accounts.isDuplicate(email_string)

    def CheckPasswordMatch(self, password1, password2):
        return Password.isMatch(password1, password2)

    # A thin wrapper to actually make the account from valid input.
    def makeAccount(self, email, password, seckey):
        return Account(Email(email), Password(password), seckey)

    def createAccount(self):
        password1 = self.view.getPassword1()
        password2 = self.view.getPassword2()

        if !self.ValidatePassword(password1):
            self.displaymessage("Password is not valid")
            return

        if !self.CheckPasswordMatch(password1, password2):
            self.displaymessage("Passwords don't match")
            return

        email = self.view.getEmail()
        if !self.ValidateEmail(email):
            self.displaymessage("Email is duplicate or incorrect format")
            return

        account = self.makeAccount(email, password, self.view.getSecKey())
        self.displaymessage("Account was created successfully")
        return

现在,验证包装器易于测试,它们接受输入并返回布尔值. makeAccount也很容易测试,它接受输入并返回一个帐户(或不返回).

Now the validation wrappers are simple to test, they take inputs and return a boolean. makeAccount is also simple to test, it takes inputs and returns an Account (or doesn't).

createAccount仍然做得太多.它处理从视图创建帐户的过程,但也显示消息.我们需要将它们分开.

createAccount is still doing too much. It handles the process of creating an account from a view, but its also displaying messages. We need to separate them.

现在该是例外的时候了!我们带回验证失败异常,但要确保它们都是CreateAccountFailed的子类.

Now is the time for exceptions! We bring back our validation failure exceptions, but making sure they're all subclasses of CreateAccountFailed.

# This is just a sketch.

class CreateAccountFailed(Exception):
    pass

class PassNotValid(CreateAccountFailed):
    pass

class PassNotMatch(CreateAccountFailed):
    pass

class EmailNotOK(CreateAccountFailed):
    pass

现在,如果createAccount无法创建帐户,则可以引发CreateAccountFailed异常的特定版本.这有很多好处.调用createAccount更安全.更灵活.我们可以将错误处理分开.

Now createAccount can throw specific versions of CreateAccountFailed exceptions if it fails to create an account. This has many benefits. Calling createAccount is safer. It's more flexible. We can separate out the error handling.

    def createAccount(self):
        password1 = self.view.getPassword1()
        password2 = self.view.getPassword2()

        if !self.ValidatePassword(password1):
            raise PassNotValid("Password is not valid")

        if !self.CheckPasswordMatch(password1, password2):
            raise PassNotMatch("Passwords don't match")

        email = self.view.getEmail()
        if !self.ValidateEmail(email):
            raise EmailNotOK("Email is duplicate or incorrect format")

        return self.makeAccount(email, password, self.view.getSecKey())

    # A thin wrapper to handle the display.
    def tryCreateAccount(self):
        try
            account = self.createAccount()
            self.displaymessage("Account was created successfully")
            return account
        except CreateAccountFailed as e:
            self.displaymessage(str(e))

哇,好多.但是现在createAccount可以轻松进行单元测试!测试它会按预期创建一个帐户.使它引发各种异常.验证方法有自己的单元测试.

Whew, that was a lot. But now createAccount can be easily unit tested! Test it will create an Account as expected. Make it throw various exceptions. The validation methods get their own unit tests.

甚至可以测试tryCreateAccount. 模拟displaymessage 并检查是否在正确的消息中调用了它正确的情况.

Even tryCreateAccount can be tested. Mock displaymessage and check that it's called with the right messages in the right situations.

总结...

  • 请勿在控制流中使用异常.
  • 在例外情况下,请使用例外,例如无法创建帐户.
  • 使用异常将错误与错误处理分开.
  • 毫不费力地将功能与显示屏分开.
  • 毫不留情地削减功能,直到他们做一件事.
  • 使用瘦包装器功能隐藏实现.
  • 不要将值放在对象上,除非您实际上需要该对象在一个方法之外记住它们.
  • 编写接受输入并返回结果的函数.没有副作用.

这篇关于如何用pytest编写正确的测试?的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!

查看全文
登录 关闭
扫码关注1秒登录
发送“验证码”获取 | 15天全站免登陆