如何编写注册账号测试用例?
How to write registering account test cases?
这是
的跟进问题
根据原始 post 的建议调整我的代码后,下面是我的完整工作代码。
但是,我有一些问题和疑问:
- 如何测试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
- createAccount()是否违反这句话?它没有接受输入的参数。
Write functions that take input and return a result. No side effects.
- "if" createAccount() 中的语句是控制流?
如果是,是否违反这句话?
**
Don't use exceptions for control flow.
** 还是我理解错了什么?
Ruthlessly shave functions down until they do one thing.
那么,为什么 createAccount() 做两件事?它从用户输入中获取值然后验证
- 我希望电子邮件输入最多再次显示 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()
可测试的目标。
- This is my test but createAccount() doesn't have parameters, so how to add input to it for testing?
createAccount
从 self.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 它们。类似于模拟,但更有条理。
- 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()
的所有内容仍然有效。
- "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()
正在做的事情。
- [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
)而无需更改其界面。
- 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 上述回答的补充(添加更多信息)。我们需要确定测试的目的是什么。我想到以下两个原因,每一个都导致使用相同策略实现模拟。
- 验证用户输入无效电子邮件 3 次后,
抛出异常。
- 验证 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).')
这是
根据原始 post 的建议调整我的代码后,下面是我的完整工作代码。
但是,我有一些问题和疑问:
- 如何测试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
- createAccount()是否违反这句话?它没有接受输入的参数。
Write functions that take input and return a result. No side effects.
- "if" createAccount() 中的语句是控制流? 如果是,是否违反这句话? **
Don't use exceptions for control flow.
** 还是我理解错了什么?
Ruthlessly shave functions down until they do one thing.
那么,为什么 createAccount() 做两件事?它从用户输入中获取值然后验证
- 我希望电子邮件输入最多再次显示 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()
可测试的目标。
- This is my test but createAccount() doesn't have parameters, so how to add input to it for testing?
createAccount
从 self.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 它们。类似于模拟,但更有条理。
- 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()
的所有内容仍然有效。
- "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()
正在做的事情。
- [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
)而无需更改其界面。
- 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 上述回答的补充(添加更多信息)。我们需要确定测试的目的是什么。我想到以下两个原因,每一个都导致使用相同策略实现模拟。
- 验证用户输入无效电子邮件 3 次后, 抛出异常。
- 验证 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).')