Unit Testing XV - Mock Objects

Using mock objects to test how often a method is executed


In the previous article in this series, we saw the User4.class.php used with our T4_UserDependencyInjectionTest to demonstrate Dependency Injection (DI). In this one, we'll modify the test to measure expectations with mock objects, rather than stubs and create a new test file, T5_UserMockingTest.php.


MODX logo

Expectations

In previous articles, we saw the importance of using unit testing to make sure each part of whatever you're testing is behaving properly. This doesn't guarantee bug-free code, however. A method that works perfectly when tested could cause havoc if it gets called more times than it should or gets called when it shouldn't. Testing for this is the primary function of mocks.

Remember that we defined a stub as returning a hard-coded value that never varies. Our stubbed Validator class returned true or false from the three validate...() methods. The methods did no calculations and didn't really test anything, they just dumbly returned what they were set to return.

A mock, on the other hand, can have a little more intelligence. Its methods can (with help from the testing platform) keep track of how many times it has been called. The platform can then report at the end of each test run whether any method was called an inappropriate number of times.

Let's modify our T4_UserDependencyInjection test to add some mocking. We'll create a new test file called T5_UserMockingTest.php. There won't be any changes to the User class, so we'll continue to use the User4.class.php file in our new test. Issue this command from the build directory:

codecept generate:test unit T5_UserMocking

The name of the test we're creating is somewhat misleading. It's not the User class that we're mocking. It's the Validator class.

Open the created file (_build/tests/T5_UserMockingTest.php) and paste the following code into it (replacing what's there). The file is also available at GitHub, here.

<?php
use Codeception\Util\Stub;
use \Codeception\Stub\Expected;

class T5_UserMockingTest extends \Codeception\Test\Unit
{

    /**
     * @var \UnitTester
     */
    protected $tester;
    protected $validator;
    protected $fields = array(
        'username' => 'BobRay',
        'email' => 'bobray@hotmail.com',
        'phone' => '218-456-1234'
    );

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

    protected function _after() {
    }

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

    /** @throws Exception */
    public function testConstructorWithParams() {
        $validator = $this->make(Validator::class,
            array(
                'validateUsername' => Expected::once(function () {
                    return true;
                }),
                'validateEmail' => Expected::once(function () {
                    return true;
                }),
                'validatePhone' => Expected::once(function () {
                    return true;
                }),
            ));
        $user = new User4($validator, $this->fields);
        assertInstanceOf('User4', $user);
        assertEquals('BobRay', $user->get('username'));
        assertEquals('bobray@hotmail.com', $user->get('email'));
        assertEquals('218-456-1234', $user->get('phone'));
        assertFalse($user->hasErrors());
    }

    /** @throws Exception */
    public function testConstructorNoParams() {
        $validator = $this->make(Validator::class, $this->fields);;
        $user = new User4($validator);
        assertEquals('', $user->get('username'));
        assertEquals('', $user->get('email'));
        assertEquals('', $user->get('phone'));
        assertFalse($user->hasErrors());
    }

    /** @throws Exception */
    public function testConstructorAllBad() {
        $validator = $this->make(Validator::class,
            array(
                'validateUsername' => Expected::once(function () {
                    return false;
                }),
                'validateEmail' => Expected::once(function () {
                    return false;
                }),
                'validatePhone' => Expected::once(function () {
                    return false;
                }),
            ));
        $fields = $this->fields;
        $fields['invalidField'] = '';
        $user = new User4($validator, $fields);

        assertTrue($user->hasErrors());
        $errors = $user->getErrors();
        assertEquals(4, count($errors), print_r($errors, true));
        assertContains('Invalid username', $errors);
        assertContains('Invalid email', $errors);
        assertContains('Invalid phone', $errors);
        assertContains('Unknown field', $errors);
    }

    /** @throws Exception */
    public function testConstructorAllGood() {
        $validator = $this->make(Validator::class,
            array(
                'validateUsername' => Expected::once(function () {
                    return true;
                }),
                'validateEmail' => Expected::once(function () {
                    return true;
                }),
                'validatePhone' => Expected::once(function () {
                    return true;
                }),

            ));

        $user = new User4($validator, $this->fields);

        assertFalse($user->hasErrors());
        $errors = $user->getErrors();
        assertEmpty($errors);
    }

    /** @throws Exception */
    public function testConstructorBadUsernameOnly() {

        $validator = $this->make(Validator::class,
            array(
                'validateUsername' => Expected::once(function () {
                    return false;
                }),
                'validateEmail' => Expected::once(function () {
                    return true;
                }),
                'validatePhone' => Expected::once(function () {
                    return true;
                }),
            ));

        $user = new User4($validator, $this->fields);

        assertTrue($user->hasErrors());
        $errors = $user->getErrors();
        assertEquals(1, count($errors), print_r($errors, true));
        assertContains('Invalid username', $errors);
    }


    /** @throws Exception */
    public function testConstructorBadEmailOnly() {

        $validator = $this->make(Validator::class,
            array(
                'validateEmail' => Expected::once(function () {
                    return false;
                }),
                'validateUsername' => Expected::once(function () {
                    return true;
                }),
                'validatePhone' => Expected::once(function () {
                    return true;
                }),
            ));

        $user = new User4($validator, $this->fields);


        assertTrue($user->hasErrors());
        $errors = $user->getErrors();
        assertEquals(1, count($errors), print_r($errors, true));
        assertContains('Invalid email', $errors);
    }

    /** @throws Exception */
    public function testConstructorBadPhoneOnly() {
        $validator = $this->make(Validator::class,
            array(
                'validateEmail' => Expected::once(function () {
                    return true;
                }),
                'validateUsername' => Expected::once(function () {
                    return true;
                }),
                'validatePhone' => Expected::once(function () {
                    return false;
                }),
            ));

        $user = new User4($validator, $this->fields);

        assertTrue($user->hasErrors());
        $errors = $user->getErrors();
        assertEquals(1, count($errors), print_r($errors, true));
        assertContains('Invalid phone', $errors);
    }

    /** @throws Exception */
    public function testConstructorUnknownFieldOnly() {

        $validator = $this->make(Validator::class,
            array(
                'validateEmail' => Expected::once(function () {
                    return true;
                }),
                'validateUsername' => Expected::once(function () {
                    return true;
                }),
                'validatePhone' => Expected::once(function () {
                    return true;
                }),
            ));

        $fields = $this->fields;
        $fields['invalidField'] = '';
        $user = new User4($validator, $fields);

        assertTrue($user->hasErrors());
        $errors = $user->getErrors();
        assertEquals(1, count($errors), print_r($errors, true));
        assertContains('Unknown field', $errors);
    }

    // tests
    public function testSomeFeature()
    {

    }
}

Changes

The first change you'll notice is the addition of this line at the top:

use \Codeception\Stub\Expected;

That allows us to use the Expected class in the code.

The rest of the changes are simpler, because all three validation methods are expected to be executed exactly once. Here's an example section:

/** @throws Exception */
public function testConstructorBadPhoneOnly() {
    $validator = $this->make(Validator::class,
        array(
            'validateEmail' => Expected::once(function () {
                return true;
            }),
            'validateUsername' => Expected::once(function () {
                return true;
            }),
            'validatePhone' => Expected::once(function () {
                return false;
            }),
        ));

    $user = new User4($validator, $this->fields);

    assertTrue($user->hasErrors());
    $errors = $user->getErrors();
    assertEquals(1, count($errors), print_r($errors, true));
    assertContains('Invalid phone', $errors);
}

In the make() array, this section:

'validateEmail' => function () {
    return true;
},

has been changed to:

'validateEmail' => Expected::once(function () {
    return true;
}),

The function has been wrapped in Expected::once(). Notice that the final ) follows the closing curly bracket of the function.

As you might expect, this tells Codeception that the method is expected to be executed exactly once in this test.

When you run the test, you won't see anything different because all the validation methods actually are executed exactly once. Try changing once to never in one of the validatePhone lines and run the test again. You should see something like this:

Validator::validatePhone('218-456-1234') was not expected to be called.

We didn't need them, but the other options are:

  • Expected::never
  • Expected::atLeastOnce
  • Expected::Exactly(3, function(){...})

If you leave out the Expected wrapper (as we did in the previous test — T4_UserDependencyInjectionTest.php), the method can be executed any number of times or no times.


Coming Up

In the next article, we'll review our progress through the various unit tests and discuss some of the issues we encountered.



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 A2 hosting. (More information in the box below.)



Comments (0)


Please login to comment.

  (Login)