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
.

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 Hosting.com (formerly A2 Hosting). (More information in the box below.)
Comments (0)
Please login to comment.