Unit Testing VII - Testing Error Handling with Stubs

Introducing better error handling and unit testing it with using stubs


In the previous article in this series, we looked at some very bad code that performed validation on the fields of the User class. In this one, we'll think about improving the error handling and testability of the class. In keeping with the concept of Test-Driven Development (TDD) we'll see what our tests will look like before modifying the class. We'll also see some stubs.


MODX logo

Error Handling

Suppose we add an array called $errors to our class. The array will be empty to begin with. Every time an error occurs, the class will add a string to the $errors array. Now anyone using our User class to create a new user object can check to see if there are any errors.

We'll need a getErrors() method that returns the array of errors. For convenience, we'll also add a hasErrors() method that returns false if the $errors array is empty, and true if it contains any errors.


Structural Changes

First, we'll stop returning from the constructor when an error is detected. When an error occurs, we'll add it to the $errors array and continue.

Second, we'll move the validation code out of the constructor and into three separate methods: validateUsername(), validateEmail(), and validatePhone(). They'll return true for a valid value and false for an invalid value.


Testing

Now that we've got a plan for the User class error system, we can create our test of it before making any changes to the class itself. The tests will fail, but should pass once we've made the changes to the class.


The Test File

In your terminal type this command:

codecept generate:test unit T2_UserError

Then load the T2_UserErrorTest.php file into your editor.

Here's the full test file:

<?php
use \Codeception\Stub;

class T2_userErrorTest extends \Codeception\Test\Unit {

    /**
     * @var $tester \UnitTester
     */
    protected $tester;
    protected $validUsername = 'BobRay';
    protected $invalidUsernameTooShort = 'Bo';
    protected $invalidUsernameTooLong =
        'asda sdada sdasdasd adasdasda' .
        'sdasda sdadadadad asdasdad';
    protected $validEmail = 'bobray@hotmail.com';
    protected $invalidEmail = 'bobrayhotmail.com';
    protected $validPhone = '218-456-2345';
    protected $invalidPhone = '218x123x2345';

    protected function _before() {
        require_once dirname(dirname(dirname(dirname(__FILE__)))) . '/core/model/user2.class.php';
    }

    protected function _after() {
    }

    public function testWorking() {
        assertTrue(true);
    }

    /** @throws /Exception */
    public function testValidateUsername() {
        $user = Stub::makeEmptyExcept('User2', 'validateUsername');
        $result = $user->validateUsername($this->validUsername);
        assertTrue($result);
        $result = $user->validateUsername($this->invalidUsernameTooShort);
        assertFalse($result);
        $result = $user->validateUsername($this->invalidUsernameTooLong);
        assertFalse($result);
    }

    /** @throws /Exception */
    public function testValidateEmail() {
        $user = Stub::makeEmptyExcept('User2', 'validateEmail');
        $result = $user->validateEmail($this->validEmail);
        assertTrue($result);
        $result = $user->validateEmail($this->invalidEmail);
        assertFalse($result);
    }

    /** @throws /Exception */
    public function testValidatePhone() {
        $user = Stub::makeEmptyExcept('User2', 'validatePhone');
        $result = $user->validatePhone($this->validPhone);
        assertTrue($result);
        $result = $user->validatePhone($this->invalidPhone);
        assertFalse($result);
    }

    public function testAllValid() {
        $fields = array(
            'username' => $this->validUsername,
            'email' => $this->validEmail,
            'phone' => $this->validPhone,
        );
        $user = new User2($fields);
        assertFalse($user->hasErrors());
        $errors = $user->getErrors();
        assertEmpty($errors);
    }

    public function testBadUsernameWithConstructor() {
        /* Too Short */
        $fields = array(
            'username' => $this->invalidUsernameTooShort,
            'email' => $this->validEmail,
            'phone' => $this->validPhone,
        );
        $user = new User2($fields);
        assertTrue($user->hasErrors());
        $errors = $user->getErrors();
        assertNotEmpty($errors);
        assertEquals(1, count($errors));
        assertEquals('Invalid username', $errors[0], $errors[0]);

        /* Too long */
        $fields = array(
            'username' => $this->invalidUsernameTooLong,
            'email' => $this->validEmail,
            'phone' => $this->validPhone,
        );
        $user = new User2($fields);
        assertTrue($user->hasErrors());
        $errors = $user->getErrors();
        assertNotEmpty($errors);
        assertEquals(1, count($errors));
        assertEquals('Invalid username', $errors[0], $errors[0]);
    }

    public function testBadEmailWithConstructor() {
        $fields = array(
            'username' => $this->validUsername,
            'email' => $this->invalidEmail,
            'phone' => $this->validPhone,
        );
        $user = new User2($fields);
        assertTrue($user->hasErrors());
        $errors = $user->getErrors();
        assertNotEmpty($errors);
        assertEquals(1, count($errors));
        assertEquals('Invalid email', $errors[0], $errors[0]);
    }

    public function testBadPhoneWithConstructor() {
        $fields = array(
            'username' => $this->validUsername,
            'email' => $this->validEmail,
            'phone' => $this->invalidPhone,
        );
        $user = new User2($fields);
        assertTrue($user->hasErrors());
        $errors = $user->getErrors();
        assertNotEmpty($errors);
        assertEquals(1, count($errors));
        assertEquals('Invalid phone', $errors[0], $errors[0]);
    }
}

The code above is available here.


How it Works

The first difference you'll notice is the use \Codeception\Stub; line at the top, just below the PHP tag. That's necessary because we'll be creating some stub versions of our User class (more on this in a bit).

Next, you'll see that we've added some class variables at the top of the class containing valid and invalid values for the fields. We did it this way because there aren't that many of them. If there were a lot more, we'd use a data provider to supply them. We'll see how to create a data provider a little later on in this series.

The first test is our testWorking() test that just makes sure the framework is working.

The next three methods test the three validation methods by themselves (validateUsername(), validateEmail(), and validatePhone(). We use Codeception's static member of the Stub class: Stub::makeEmptyExcept().

The Codeception make... methods create stub versions of a class. The methods are documented here. The use \Codeception\Stub; line allows us to use them this way. They can also be called like this: $this->make.... I've used the static call because it's a little easier to type, and to show that they are static methods, but you can do it either way.

Using make ... creates a stub of the class without calling the class constructor. This is particularly important in our example because calling the actual constructor would execute all three methods every time. Since we only want to test a single method, we don't want that (though we'll call the actual constructor in other tests in this test file).

Here, we've used makeEmptyExcept('someFunction'). That creates a User class that's completely empty (all methods return null) *except* the method we're testing at the moment. Consider this code:

$user = Stub::makeEmptyExcept('User', 'validateUsername');

The first parameter is the name of the class we're stubbing. The second is the name of the only method that should keep its real code.

The code above creates a stub User class which is almost completely empty, but where the validateUsername() method contains the real code of that method. Since we're only testing that method, we don't really need the rest of the class, so it makes sense to cut down on the overhead of all the other class methods. This is particularly useful when testing very large classes that take time to instantiate.

Stub::make... also takes an optional third parameter containing an associative array of keys and values. If the key is a class variable, Codeception will set that variable to the value associated with that key. If the key is a method name, rather than a class variable, that method will return the associated value.

You can set both class variables and method return values in the array in the third parameter (though we haven't done so here). Codeception will figure out whether the key is a class variable or a method (assuming that you don't have a variable and a method with the same name). For methods, you can even make the value an anonymous function that performs calculations before returning a value.

Here's an example:

$user = Stub::makeEmptyExcept('User', 'save',
    ['isValid' => function () { return true; }]);

In the example above, the User class stub is created without calling the constructor. The save() method will retain its original code. The isValid() method will always return true, no matter what input it receives.


Drilling Down

Let's take a look at the code for a single test:

    /** @throws /Exception */
    public function testValidatePhone() {
        $user = Stub::makeEmptyExcept('User', 'validatePhone');
        $result = $user->validatePhone($this->validPhone);
        assertTrue($result);
        $result = $user->validatePhone($this->invalidPhone);
        assertFalse($result);
    }

The comment, /** @throws /Exception */, is just to satisfy your code editor. A good code editor will warn you that there's an unhandled exception in the method because one or more of the methods called throw exceptions when they fail. The code would run without the comment — it just suppresses the warning.

The makeEmptyExcept() line creates a stubbed version of the User class, with all methods empty (returning null) except the validatePhone method, which retains its original code. That's fine with us because we're not calling any of them.

First, we call the User2 class validatePhone() method with a valid phone number and make sure it returns true. Then we call it with an invalid phone number and make sure it returns false. The constructor is never called, and if we tried to call it directly, it would do nothing.

After this test, we do essentially the same thing with the other two methods, validateUsername(), and validateEmail(), except that the username test checks with both too-short and too-long usernames. Note that in real life, the tests would use many more cases of valid and invalid values. We'll see examples of that that in upcoming articles.


The "WithConstructor ..." Tests

The rest of the methods in our test file actually call the real constructor, which makes them a little more complicated. They create an actual, standard User2 class. Let's look at an example:

public function testBadEmailWithConstructor() {
    $fields = array(
        'username' => $this->validUsername,
        'email' => $this->invalidEmail,
        'phone' => $this->validPhone,
    );
    $user = new User2($fields);
    assertTrue($user->hasErrors());
    $errors = $user->getErrors();
    assertNotEmpty($errors);
    assertEquals(1, count($errors));
    assertEquals('Invalid email', $errors[0], $errors[0]);
}

First, the code above sets up the array of user fields to send to the constructor, all with valid values except the email field. Then we create a User2 object with $user = new User2($fields);.

Since the bad email should create an error, we make sure $user->hasError() returns true. Then, we get the error array with $user->getErrors();.

Next, we make sure that the error array is not empty, and then we make sure that it has only one error by asserting that 1 and count($errors) are equal. For all assertions that take two or more parameters, the expected value is always the first one.

Finally, we need to make sure that the error message returned in the $errors array is the correct one. We do that in the last line by asserting that the string 'Invalid email' (and only) equals the first, and only member of the $errors array.

Notice that, for the last line, we've added an optional error message as the third parameter: $errors[0]. If the assertion fails, the console will show the actual content of the first (and in this case, only) error message in the $errors array. This will help a lot if we've misspelled the error message in the test code. For strings, assertEquals requires an exact match, so if the real message is Invalid email and our test code uses Invalid Email, the assertion will fail. In that case, the message in the third parameter will let us see why the assertion is failing.


Coming Up

In the next article, we'll take a look at our new User2 class, altered so that the tests above will pass.



For more information on how to use MODX to create a web site, see my web site Bob's Guides, or better yet, buy my book: MODX: The Official Guide.

Looking for high-quality, MODX-friendly hosting? As of May 2016, Bob's Guides is hosted at Hosting.com (formerly A2 Hosting). (More information in the box below.)



Comments (0)


Please login to comment.

  (Login)