[英]How to write registering account test cases?
This is a follow up question on this post 这是此帖子的后续问题
After tweak my code as suggestion on the original post, below is my full working code. 在对我的代码进行了调整之后,下面是我的完整工作代码。
However, I have some problems and questions: 但是,我有一些问题和疑问:
This is my test but createAccount() doesn't have parameters, so how to add input to it for testing ? 这是我的测试,但是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
Write functions that take input and return a result.
编写接受输入并返回结果的函数。 No side effects.
没有副作用。
Don't use exceptions for control flow.
不要将异常用于控制流。
** Or I misunderstand about something ? **还是我误会了某些东西?
Ruthlessly shave functions down until they do one thing.
无情地削减职能,直到他们做一件事。
So, why createAccount() do 2 things ? 那么,为什么createAccount()做2件事? It get value from user input then validate
它从用户输入中获取价值,然后进行验证
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()
Note: the code in the other answer is not the best code, but it's a vast improvement over where we started. 注意: 另一个答案中的代码不是最好的代码,但是它对我们从何处开始进行了巨大的改进。 Part of refactoring is knowing when it's good enough.
重构的一部分是知道什么时候足够好。 Keep in mind as you read this, there are more improvements which could be made, but the goal of making
createAccount()
testable was achieved. 请记住,阅读本文时,可以进行更多的改进,但是可以实现
createAccount()
可测试的目标。
- This is my test but createAccount() doesn't have parameters, so how to add input to it for testing?
这是我的测试,但是createAccount()没有参数,那么如何为测试添加输入呢?
createAccount
gets its information from self.view
. createAccount
从self.view
获取其信息。 That's a RegisterUI
object. 那是一个
RegisterUI
对象。 RegisterUI
's methods are interactive which makes them difficult to use in tests. RegisterUI
的方法是交互式的,这使得它们很难在测试中使用。
Fortunately we can pass any view we like to RegisterController
. 幸运的是,我们可以将我们喜欢的任何视图传递给
RegisterController
。 We're not testing RegisterUI
, it should have its own tests, just how RegisterController
uses RegisterUI
. 我们不测试
RegisterUI
,它应该具有自己的测试,就像RegisterController
如何使用RegisterUI
。 So we'll make a version of RegisterUI
just for testing and use that. 因此,我们将创建一个
RegisterUI
版本,仅用于测试和使用。
We can make a Mock object that responds to RegisterUI
's methods. 我们可以制作一个响应
RegisterUI
方法的Mock对象 。
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()
will return the email
and so on. mock_view.getEmail()
将返回email
,依此类推。 Use that as the controller's view and go. 使用它作为控制器的视图。
ctrl = RegisterController(mock_view)
account = ctrl.createAccount()
assert account.email == email
assert account.password == password1
assert account.seckey == seckey
Alternatively you can write a subclass of RegisterUI
just for testing which takes it's attributes in the constructor and overrides getEmail()
and friends to return them. 另外,您可以编写
RegisterUI
的子类以进行测试,该子类在构造函数中使用其属性,并覆盖getEmail()
和friends以返回它们。 Similar to a mock, but a little more organized. 与模拟游戏类似,但更有条理。
- Does createAccount() violate [Write functions that take input and return a result.
createAccount()是否违反[编写接受输入并返回结果的函数。 No side effects.]?
没有副作用。]? It doesn't have parameters that take input.
它没有接受输入的参数。
Technically yes, but that's a rule of thumb. 从技术上说是的,但这是一个经验法则。 You could pass in the
view
instead of using self.view
, but the whole point of a controller is to bridge the gap between the view and the models. 您可以传递
view
而不是使用self.view
,但是控制器的重点是弥合视图和模型之间的差距。 It's appropriate that it would have access to the UI. 可以访问UI是适当的。
createAccount()
is an integration function. createAccount()
是一个集成函数。 It encapsulates the process of creating an account using information from the UI; 它封装了使用UI中的信息创建帐户的过程; no knowledge of the details of the UI nor the account is required.
无需了解用户界面的详细信息,也无需帐户。 This is good.
很好 You can change the account creation process and everything that calls
createAccount()
will still work. 您可以更改帐户创建过程,所有调用
createAccount()
仍然可以使用。
- "if" statement in createAccount() is control flow?
createAccount()中的“ if”语句是控制流吗? If yes, [is this using exceptions for control flow?]
如果是,[这是否在控制流中使用异常?]
Yes, an if
is control flow. 是的,
if
是控制流程。 But createAccount()
is not using exceptions for control flow. 但是
createAccount()
并未将异常用于控制流。
Exceptions are for exceptional cases. 例外是例外情况。
open
opens a file. open
打开一个文件。 If it fails to open a file you get an exception. 如果无法打开文件,则会出现异常。
createAccount()
creates an account. createAccount()
创建一个帐户。 If it fails to create an account that is exceptional, so it throws an exception. 如果无法创建例外帐户,则抛出异常。
Contrast this with a function like isEmailValid(email)
. 将此与
isEmailValid(email)
类的函数进行对比。 This is asking whether an email is valid or not. 这是在询问电子邮件是否有效。 Using an exception to indicate an invalid email would be inappropriate;
使用异常指示无效的电子邮件是不合适的; it is totally expected that
isEmailValid(email)
will be given an invalid email. 完全预期
isEmailValid(email)
将收到无效的电子邮件。 An invalid email is a normal condition for isEmailValid
. 无效的电子邮件是
isEmailValid
的正常情况。 Instead it should return a simple boolean. 相反,它应该返回一个简单的布尔值。
However, isEmailValid(email)
might use exceptions to indicate why the email was invalid. 但是,
isEmailValid(email)
可能使用异常来指示为什么电子邮件无效。 For example, it could throw EmailIsDuplicate
to indicate a duplicate and EmailIsInvalid
to indicate it's a formatting problem. 例如,它可能抛出
EmailIsDuplicate
表示重复,而EmailIsInvalid
表示这是格式问题。
def ValidateEmail(self, email):
email_obj = Email(email)
if !accounts.isDuplicate(email):
raise EmailIsDuplicate()
if !email_obj.isValidEmail():
raise EmailIsInvalid()
return true
Then the caller could use the exception to display an appropriate error. 然后,调用者可以使用异常来显示适当的错误。
try:
self.ValidateEmail(email)
except EmailIsDuplicate
self.displaymessage("That email is already registered.")
except EmailIsInvalid
self.displaymessage("The email is not formatted correctly.")
Which is what createAccount()
is doing. createAccount()
在做什么。
- [If I should "ruthlessly shave functions down until they do one thing", why does] createAccount() do 2 things ?
[如果我应该“无情地削减函数,直到它们做一件事”,为什么] createAccount()做2件事? It get value from user input then validates.
它从用户输入中获取价值,然后进行验证。
From the outside perspective it does one thing: it handles creating an account from user input. 从外部的角度来看,它做一件事:它负责根据用户输入创建帐户。 Exactly how it does that is deliberately a black box.
确切的说,这是一个黑匣子。 This information hiding means if the details of how creating an account works changes, the effects on the rest of the program are limited.
该信息隐藏意味着,如果创建帐户的工作方式的详细信息发生更改,则对程序其余部分的影响将受到限制。
If later its decided that an account needs a name, you can add that to createAccount()
(and RegisterUI.getName
) without changing its interface. 如果稍后它决定一个帐户需要一个名称,则可以将该名称添加到
createAccount()
(和RegisterUI.getName
)中,而无需更改其接口。
- I want to [as the user for a valid email up to 3 times].
我想[作为有效电子邮件的用户最多3次]。 After that, app raises exception.
之后,应用程序引发异常。 How to do that for easy testing?
如何进行简单测试?
When I was working on your code yesterday I didn't realize self.view.getEmail()
was interactive! 昨天在处理您的代码时,我没有意识到
self.view.getEmail()
是交互式的! That explains the infinite loops. 那解释了无限循环。 I didn't understand that.
我不明白。
We'd add another method to encapsulate asking for a valid email. 我们将添加另一种方法来封装请求有效的电子邮件。
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
Similarly we'd fold asking for the password and verifying it into one method. 同样,我们将折叠询问密码并将其验证为一种方法。 Now I understand what the
while 1
was for, you want to ask until they give you a valid password. 现在,我了解了
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
And then createAccount()
calls them making it even slimmer. 然后
createAccount()
调用它们,使其更纤薄。
def createAccount(self):
email = self.AskForValidEmail()
password = self.AskForValidPassword()
return self.makeAccount(email, password1, self.view.getSecKey())
To test AskForValidEmail
you can make a fancier RegisterUI
mock. 要测试
AskForValidEmail
您可以制作一个更好的RegisterUI
模拟。 Instead of getEmail
just returning a string, it can return an invalid email on the first two calls and a valid email on the third. 代替
getEmail
仅返回字符串,它可以在前两个调用中返回无效的电子邮件,而在第三个调用中返回有效的电子邮件。
This is supplement (add more information) to Schwern's answer above. 这是上述Schwern答案的补充(添加更多信息)。 We need to determine what is the purpose of the test.
我们需要确定测试的目的。 I think of two reasons below, each one lead to an implement of mocking using same strategy.
我认为以下两个原因,每个原因导致使用相同策略实现模拟。
The strategy is to have a global array (in case there is object for mocking, use the object's attribute instead) to keep track how many time a mocking has been called. 该策略是要有一个全局数组(如果有要模拟的对象,请改用对象的属性)以跟踪调用模拟的次数。 Below is the suggestion.
以下是建议。
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
After that you can have 2 test functions. 之后,您可以拥有2个测试功能。 One uses mock_3_failed then assert that exception is thrown.
一个使用了mock_3_failed,然后断言抛出了异常。 The other one uses mock_3rd_good then assert the expected result is returned.
另一个使用mock_3rd_good然后断言返回了预期的结果。
Another supplement is to refactor "raise/try" control flow. 另一个补充是重构“提升/尝试”控制流。 Currently we store logic knowledge at two places: ValidateEmail function for checking, AskForValidEmail for reporting error.
当前,我们将逻辑知识存储在两个位置:ValidateEmail函数用于检查,AskForValidEmail用于报告错误。 Instead, we can refactor to only one place: ValidateEmail function.
相反,我们只能将其重构到一个位置:ValidateEmail函数。 That will help in future code change.
这将有助于将来的代码更改。
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).')
声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.