简体   繁体   English

如何编写注册帐户测试用例?

[英]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: 但是,我有一些问题和疑问:

  1. How to test createAccount () that can create account successfully or can throw exception ? 如何测试可以成功创建帐户或引发异常的createAccount ()?

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
  1. Does createAccount() violate this sentence ? createAccount()是否违反了这句话? It doesn't have parameters that take input. 它没有接受输入的参数。

Write functions that take input and return a result. 编写接受输入并返回结果的函数。 No side effects. 没有副作用。

  1. "if" statement in createAccount() is control flow ? createAccount()中的 “ if”语句是控制流吗? If yes, whether it violate this sentence ? 如果是,是否违反了这句话? ** **

Don't use exceptions for control flow. 不要将异常用于控制流。

** Or I misunderstand about something ? **还是我误会了某些东西?

  1. 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 它从用户输入中获取价值,然后进行验证

  1. I want email input will be shown again up to 3 times. 我希望电子邮件输入最多显示3次。 After that, app raises exception. 之后,应用程序引发异常。 How to do that for easy testing ? 如何进行简单测试?


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()可测试的目标。


  1. 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 . createAccountself.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. 与模拟游戏类似,但更有条理。

  1. 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()仍然可以使用。

  1. "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()在做什么。

  1. [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 )中,而无需更改其接口。

  1. 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. 我认为以下两个原因,每个原因导致使用相同策略实现模拟。

  1. To verify that after exact 3 times user enters invalid email, the exception is thrown. 为了验证用户输入正确的电子邮件3次后是否抛出异常。
  2. To verify that after 2 invalid times, user enter valid email at 3rd time. 为了验证两次无效时间后,用户在第三次输入有效的电子邮件。

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.

相关问题 Robotframework 测试用例,topn 如何为这个场景编写测试用例 - Robotframework test cases , how topn write a test cases for this scenaries 如何在烧瓶中编写经过身份验证的URL的测试用例 - how to write test cases for authenticated urls in flask 如何通过 python 中的单元测试为 init function 编写测试用例? - How to write test cases for a init function by Unit test in python? 如何在Django中为简单用户和超级用户编写测试用例 - How to write test cases for Simple Users and Super Users in Django 如何在机器人框架中使用 iframe 编写测试用例 - how to write test cases using iframe in robot framework 如何编写单元测试用例以提供Rester服务(烧瓶)的上传和下载功能? - How to write Unit test cases for upload, download functionality of Rester services(flask)? 我如何编写一个 for 循环以便它测试我的所有 5 个测试用例? - How can I write a for loop so that it tests all 5 of my test cases? Django - 编写一个在所有测试用例之前运行一次的测试 function - Django - Write a test function that runs once before all test cases 如何在测试发现中跳过一些测试用例? - How to skip some test cases in test discovery? 注册和删除帐户 - Registering and Deleting an Account
 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM