如何用pytest编写正确的测试?

时间:2019-08-05 02:59:41

标签: python unit-testing testing automated-tests pytest

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

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

  1. 验证电子邮件

  2. 验证密码

  3. 检查密码是否匹配

  4. 实例化新帐户对象

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

例如:

TestCase0。电子邮件无效

TestCase1。应用程序重试3次后停止运行

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

TestCase3。电子邮件可以,密码有效,第二个密码与第一个密码不匹配

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

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

  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)

1 个答案:

答案 0 :(得分:2)

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


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

首先,对这四个片段的每个片段执行Extract Method refactoring,以将其转变为自己的方法。我只要做三个验证步骤之一,它们基本上是相同的。由于这是死记硬背的操作,因此我们可以安全地执行此操作。 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不会在密码错误的情况下停止运行。


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

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

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

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

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

现在,我们看到ValidatePassword在做两项无关的事情:设置密码和验证密码。设置密码应该在其他地方进行。

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

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

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

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

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

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

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

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

几乎没有什么可以测试!通过此操作,我们了解了实际情况,让我们将它们重新整合在一起。 createAccount到底在做什么?

  1. self.view获取内容并将其设置在self上。
  2. 验证那些东西。
  3. 如果它们无效则显示一条消息。
  4. 创建帐户。
  5. 显示成功消息。

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

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

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

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

这是现在的样子。

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也很容易测试,它接受输入并返回一个帐户(或不返回)。


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

现在该是例外的时候了!我们带回验证失败异常,但要确保它们都是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更安全。更灵活。我们可以将错误处理分开。

    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可以轻松进行单元测试了!测试它会按预期创建一个帐户。使其引发各种异常。验证方法有自己的单元测试。

甚至tryCreateAccount都可以测试。 Mock displaymessage并检查是否在正确的情况下以正确的消息调用了它。


总结...

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