简体   繁体   中英

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 ?

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

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 ? 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 ? 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 ? It get value from user input then validate

  1. I want email input will be shown again up to 3 times. 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.


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

createAccount gets its information from self.view . That's a RegisterUI object. RegisterUI 's methods are interactive which makes them difficult to use in tests.

Fortunately we can pass any view we like to RegisterController . We're not testing RegisterUI , it should have its own tests, just how RegisterController uses RegisterUI . So we'll make a version of RegisterUI just for testing and use that.

We can make a Mock object that responds to RegisterUI 's methods.

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. 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. Similar to a mock, but a little more organized.

  1. Does createAccount() violate [Write functions that take input and return a result. 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. It's appropriate that it would have access to the UI.

createAccount() is an integration function. It encapsulates the process of creating an account using information from the 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.

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

Yes, an if is control flow. But createAccount() is not using exceptions for control flow.

Exceptions are for exceptional cases. open opens a file. If it fails to open a file you get an exception. createAccount() creates an account. If it fails to create an account that is exceptional, so it throws an exception.

Contrast this with a function like 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. An invalid email is a normal condition for isEmailValid . Instead it should return a simple boolean.

However, isEmailValid(email) might use exceptions to indicate why the email was invalid. For example, it could throw EmailIsDuplicate to indicate a duplicate and EmailIsInvalid to indicate it's a formatting problem.

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.

  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.

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.

  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?

When I was working on your code yesterday I didn't realize self.view.getEmail() was interactive! 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.

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.

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. 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.

This is supplement (add more information) to Schwern's answer above. 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.
  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. One uses mock_3_failed then assert that exception is thrown. The other one uses mock_3rd_good then assert the expected result is returned.

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. Instead, we can refactor to only one place: ValidateEmail function. 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).')

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM