如何编写注册账号测试用例?

How to write registering account test cases?

这是

的跟进问题

根据原始 post 的建议调整我的代码后,下面是我的完整工作代码。

但是,我有一些问题和疑问:

  1. 如何测试createAccount()可以成功创建账户还是抛出异常?

这是我的测试,但是 createAccount() 没有参数,那么如何向它添加输入以进行测试?

def test_canCreateAccount(ctrl):
    #valid email and password
    email = 'hello@gmail.com'
    password1 = 'beautiful'
    password2 = 'beautiful'
    account = ctrl.createAccount()
    assert account.email == email
    assert account.password == password1
  1. createAccount()是否违反这句话?它没有接受输入的参数。

Write functions that take input and return a result. No side effects.

  1. "if" createAccount() 中的语句是控制流? 如果是,是否违反这句话? **

Don't use exceptions for control flow.

** 还是我理解错了什么?

  1. Ruthlessly shave functions down until they do one thing.

那么,为什么 createAccount() 做两件事?它从用户输入中获取值然后验证

  1. 我希望电子邮件输入最多再次显示 3 次。之后,应用程序引发异常。如何做到这一点以便于测试?


class CreateAccountFailed(Exception):
    pass

class PassNotValid(CreateAccountFailed):
    pass

class PassNotMatch(CreateAccountFailed):
    pass

class EmailNotOK(CreateAccountFailed):
    pass


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):
        email_obj = Email(email)
        return email_obj.isValidEmail() and not accounts.isDuplicate(email)

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

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

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

    def createAccount(self):
        email = self.view.getEmail()
        if not self.ValidateEmail(email):
            raise EmailNotOK("Duplicate or incorrect format")

        password1 = self.view.getPassword1()
        if not self.ValidatePassword(password1):
            raise PassNotValid("Password is not valid")

        password2 = self.view.getPassword2()
        if not self.CheckPasswordMatch(password1, password2):
            raise PassNotMatch("Passwords don't match")

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

    def tryCreateAccount(self):
        try:
            account = self.createAccount()
            self.displaymessage("Account was created successfully")
            return account
        except CreateAccountFailed as e:
            self.displaymessage(str(e))

class Register(Option):
    def execute(self):
        view = RegisterUI()
        controller_one = RegisterController(view)
        controller_one.tryCreateAccount()


注意: 不是最好的代码,但它比我们开始的地方有了很大的改进。重构的一部分是知道什么时候足够好。请记住,当您阅读本文时,还可以进行更多改进,但实现了 createAccount() 可测试的目标。


  1. This is my test but createAccount() doesn't have parameters, so how to add input to it for testing?

createAccountself.view 获取信息。那是一个 RegisterUI 对象。 RegisterUI 的方法是交互式的,这使得它们很难在测试中使用。

幸运的是,我们可以将任何我们喜欢的视图传递给 RegisterController。我们没有测试 RegisterUI,它应该有自己的测试,只是 RegisterController 如何使用 RegisterUI。因此,我们将制作一个 RegisterUI 版本,仅用于测试并使用它。

我们可以制作一个 Mock object 来响应 RegisterUI 的方法。

from unittest.mock import Mock
attrs = {
  'getEmail.return_value': email,
  'getPassword1.return_value': password1,
  'getPassword2.return_value': password2,
  'getSecKey'.return_value': seckey
}
mock_view = Mock(**attrs)

mock_view.getEmail()会returnemail等等。将其用作控制器的视图并继续。

ctrl = RegisterController(mock_view)

account = ctrl.createAccount()
assert account.email == email
assert account.password == password1
assert account.seckey == seckey

或者你可以写一个 RegisterUI 的子类只是为了测试它在构造函数中获取它的属性并覆盖 getEmail() 和朋友 return 它们。类似于模拟,但更有条理。

  1. Does createAccount() violate [Write functions that take input and return a result. No side effects.]? It doesn't have parameters that take input.

从技术上讲是的,但这是经验法则。您可以传入 view 而不是使用 self.view,但控制器的全部意义在于弥合视图和模型之间的差距。它可以访问 UI.

是合适的

createAccount()是积分函数。它封装了使用 UI 中的信息创建帐户的过程;不需要了解 UI 的详细信息或帐户。这很好。您可以更改帐户创建过程,调用 createAccount() 的所有内容仍然有效。

  1. "if" statement in createAccount() is control flow? If yes, [is this using exceptions for control flow?]

是的,if 是控制流。但是 createAccount() 没有使用异常来控制流程。

例外情况适用于特殊情况open 打开一个文件。如果它无法打开文件,则会出现异常。 createAccount() 创建一个帐户。如果创建异常帐户失败,则会抛出异常。

将此与 isEmailValid(email) 等函数进行对比。这是在询问电子邮件是否有效。使用异常来指示无效电子邮件是不合适的;完全可以预料 isEmailValid(email) 会收到一封无效的电子邮件。无效的电子邮件是 isEmailValid 的正常情况。相反,它应该 return 一个简单的布尔值。

但是,isEmailValid(email) 可能会使用例外来说明电子邮件无效的原因。例如,它可以抛出 EmailIsDuplicate 表示重复,抛出 EmailIsInvalid 表示格式问题。

def ValidateEmail(self, email):
    email_obj = Email(email)
    if !accounts.isDuplicate(email):
        raise EmailIsDuplicate()
    if !email_obj.isValidEmail():
        raise EmailIsInvalid()
    return true

然后调用者可以使用异常来显示适当的错误。

try:
    self.ValidateEmail(email)
except EmailIsDuplicate
    self.displaymessage("That email is already registered.")
except EmailIsInvalid
    self.displaymessage("The email is not formatted correctly.")

createAccount() 正在做的事情。

  1. [If I should "ruthlessly shave functions down until they do one thing", why does] createAccount() do 2 things ? It get value from user input then validates.

从外部的角度来看,它只做一件事:它负责根据用户输入创建帐户。它究竟是如何做到的是故意的黑匣子。这 information hiding 意味着如果创建帐户的工作细节发生变化,对程序其余部分的影响是有限的。

如果以后决定某个帐户需要一个名称,您可以将其添加到 createAccount()(和 RegisterUI.getName)而无需更改其界面。

  1. I want to [as the user for a valid email up to 3 times]. After that, app raises exception. How to do that for easy testing?

昨天我在编写您的代码时没有意识到 self.view.getEmail() 是交互式的!这解释了无限循环。我没看懂。

我们将添加另一种方法来封装请求有效电子邮件。

def AskForValidEmail(self):
    for x in range(0, 3):
        email = self.view.getEmail()
        if self.ValidateEmail(email):
            return email
        else:
            self.displaymessage("Email was invalid or a duplicate, please try again")
    raise EmailNotOK

同样,我们会将询问密码和验证密码合并为一种方法。现在我明白 while 1 是干什么用的了,你想问,直到他们给你一个有效的密码。

def AskForValidPassword(self):
    while 1:
        password1 = self.view.getPassword1()
        password2 = self.view.getPassowrd2()
        if !Password.isMatch(password1, password2):
            self.displaymessage("The passwords do not match")
        elif !Password.isValidPassword(password):
            self.displaymessage("The password is invalid")
        else
            return password1

然后 createAccount() 打电话给他们让它变得更苗条。

def createAccount(self):
    email = self.AskForValidEmail()
    password = self.AskForValidPassword()
    return self.makeAccount(email, password1, self.view.getSecKey())

要测试 AskForValidEmail,您可以制作一个更高级的 RegisterUI 模拟。 getEmail 只是 return 一个字符串,它可以 return 在前两次调用中发送无效电子邮件,在第三次调用中发送有效电子邮件。

这是对 Schwern 上述回答的补充(添加更多信息)。我们需要确定测试的目的是什么。我想到以下两个原因,每一个都导致使用相同策略实现模拟。

  1. 验证用户输入无效电子邮件 3 次后, 抛出异常。
  2. 验证 2 次无效后,用户在第 3 次输入有效电子邮件。

策略是使用一个全局数组(如果有用于模拟的对象,请改用对象的属性)来跟踪调用了多少次模拟。以下是建议。

count_try = [
    'mock_3_failed': 0,
    'mock_3rd_good': 0,
    ]

def mock_3_failed():
    values = ['1st', '2nd', '3rd']
    current_count = count_try['mock_3_failed']
    result = values[current_count]
    # When count reaches len(values) - 1 (2 for 3 element list), reset to 0
    count_try['mock_3_failed'] = (current_count + 1
            ) if current_count < len(values) - 1 else 0
    return result

def mock_3rd_good():
    values = ['1st', '2nd', 'third@company.com']
    current_count = count_try['mock_3rd_good']
    result = values[current_count]
    count_try['mock_3_failed'] = (current_count + 1
            ) if current_count < len(values) - 1 else 0
    return result

之后你可以有2个测试函数。一个使用 mock_3_failed 然后断言抛出异常。另一个使用 mock_3rd_good 然后断言返回预期结果。

另一个补充是重构"raise/try"控制流程。目前我们将逻辑知识存储在两个地方:ValidateEmail 函数用于检查,AskForValidEmail 用于报告错误。相反,我们只能重构一个地方:ValidateEmail 函数。这将有助于将来的代码更改。

def ValidateEmail(self, email):
    email_obj = Email(email)
    if !accounts.isDuplicate(email):
        raise EmailNotOK("That email is already registered.")
    if !email_obj.isValidEmail():
        raise EmailNotOK("The email is not formatted correctly.")
    return true

def AskForValidEmail(self):
    MAX_TRY = 3
    for x in range(0, MAX_TRY):
        email = self.view.getEmail()
        try:
            self.ValidateEmail(email)
        except EmailNotOK as e:
            self.displaymessage(str(e))
    raise EmailNotOK('Reached max number of trying (%d).')