繁体   English   中英

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

[英]How to write correct test with pytest?

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

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

  1. 验证电子邮件

  2. 验证密码

  3. 检查密码匹配

  4. 实例化新帐户对象

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

例如:

TestCase0。 电子邮件无效

TestCase1。 重试电子邮件3次后,应用停止

TestCase2。 电子邮件没问题,密码无效

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

TestCase4。 电子邮件正常,密码有效,密码匹配,安全有效

TestCase5。 电子邮件正常,密码为vailid,密码匹配,安全有效,帐户创建成功

  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可能不完全正确,但技术是。


答案在于您对createAccount的描述。 它做了太多事情。 它包含各种验证方法的包装器。 它显示消息。 它创建了一个帐户。 它需要重构才能测试。 测试和重构是齐头并进的。

首先,对四个部分中的每个部分执行一个Extract Method重构 ,将它们转换为自己的方法。 我只会做三个验证步骤中的一个,它们基本上都是一样的。 由于这是一个死记硬背的操作,我们可以安全地进行。 您的IDE甚至可以为您执行重构

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这段代码就会发现一个错误:如果密码错误, 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 模拟显示displaymessage并检查是否在正确的情况下使用正确的消息调用它。


总结一下...

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

暂无
暂无

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM