[英]How to write correct test with pytest?
我可以编写一些单元测试但不知道如何编写关于将其他函数连接在一起的createAccount()的测试。
createAccount()按顺序包含一些步骤:
验证电子邮件
验证密码
检查密码匹配
实例化新帐户对象
每一步都有一些测试用例。 所以,我的问题是:1。如何编写createAccount()测试用例? 我应该列出所有可能的组合测试用例然后测试它们。
例如:
TestCase0。 电子邮件无效
TestCase1。 重试电子邮件3次后,应用停止
TestCase2。 电子邮件没问题,密码无效
TestCase3。 电子邮件正常,密码有效,第二个密码与第一个密码不匹配
TestCase4。 电子邮件正常,密码有效,密码匹配,安全有效
TestCase5。 电子邮件正常,密码为vailid,密码匹配,安全有效,帐户创建成功
这是我的代码:
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
真的在做什么?
self.view
获取事物并将它们设置为self
。 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.