In the previous article in this series, we saw our new User3.class.php
code, which calls the methods of the Validator
class to make sure the fields contain valid values. At the end of that article, I discussed the problems with the design of both the test and the class. In this one, I'll write about the Inversion of Control (IOC) method called Dependency Injection (DI) to solve them.

The Problems
To Review, here are some problems with the design and tests we discussed in the previous article:
- Our test is misusing the concept of stubs. It's a pretty well-accepted principle that you shouldn't test a stubbed class. You should use stubs and mocks to test a class, but not by stubbing or mocking the class itself.
- If you're using
new
in your constructor, you're probably doing something wrong. - Our
User
class and ourValidator
class are still too dependent on each other. Technically, they are too "tightly coupled."
Inversion of Control
The fact that our User
class loads and instantiates the Validator
class puts the User
class in control of the validation process, even though there is no actual validation code in the User
object. It's not really the job of User
to create a validator. Inversion of Control moves that job outside of the User
class.
There are a number of methods to use in implementing IOC. In this article, we'll look at Dependency Injection (DI). The other methods are beyond the scope of this article. If you'd like to read about them or the history of IOC, see this Wikipedia article.
Dependency Injection (DI)
Dependency Injection (DI) is a popular way to make classes more independent of one another (technically, more "loosely coupled)."
In DI, rather than have a class create and configure classes it needs to operate (sometimes called collaborators), you create and (if necessary) configure them outside of the class and "inject" them into the class as parameters to some method.
In our example, we'll be using what's called "constructor injection" because we'll be passing our Validator
object to the User
class constructor as a parameter.
Once we do this, the User
object no longer has "control" of the Validator
class. Control has been "inverted" — it now resides in whoever is making use of the User
class.
A bonus of this process is that we can now stub the Validator
class in our tests of the User
class, rather than stubbing the User
class itself.
Once we implement DI, the Validator
class can be moved, edited, or replaced without affecting the User
class code at all, as long as the Validator
class provides the methods needed by the User
class. Even if it doesn't, a subclass of the Validator
class can provide those methods, or an adapter with those methods can be passed to the User
class.
(If you're an experienced software developer, it may be occurring to you that an interface should be created for the Validator class. You're right, but doing so is beyond the scope of this article.)
Changes
To implement DI, we're going to have to make changes to both our test and or class. The class changes will be simpler, we'll just do the following:
- Remove the
require_once 'validator.class.php';
line - Add a
$validator
parameter to the constructor - Replace
$this->validator = new Validator();
with$this->validator = $validator;
in the constructor .
We already have a $validator
class variable. Nothing else has to change.
In the test, we'll need to "include" the Validator
class file. We'll also have to create an instance (instantiate) a stubbed Validator
class object and pass it to the User
class constructor whenever we create a new user object. We'll also need to change all references to User3
to User4
and make our tests on the $user
object rather than the stub.
The T4_UserDependencyInjectionTest Code
Issue this command in the _build
directory to create the test file:
codecept generate:test unit T4_UserDependencyInjection
Here's the Code (also available at GitHub here) :
<?php use Codeception\Util\Stub; class T4_UserDependencyInjectionTest 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' => function () { return true; }, 'validateEmail' => function () { return true; }, 'validatePhone' => 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' => function () { return false; }, 'validateEmail' => function () { return false; }, 'validatePhone' => 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' => function () { return true; }, 'validateEmail' => function () { return true; }, 'validatePhone' => 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' => function () { return false; }, 'validateEmail' => function () { return true; }, 'validatePhone' => 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' => function () { return false; }, 'validateUsername' => function () { return true; }, 'validatePhone' => 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' => function () { return true; }, 'validateUsername' => function () { return true; }, 'validatePhone' => 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' => function () { return true; }, 'validateUsername' => function () { return true; }, 'validatePhone' => 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); } }
What's New?
Our first change is to require the Validator
class file in the _before()
method. We've also added a $fields
variable at the top, since, with a few exceptions, we can use the same array of user fields for every test. The only exceptions are the two tests that test for an invalid (unknown) field.
Because we're stubbing the Validator
class, for most tests, it doesn't actually matter what values are in the $fields
array, as long as they are the actual fields expected by the User
object. The only test that really cares is the testConstructorWithParams()
method, which verifies that the right values get to the right fields. For most of the other tests, the values could be blank as long as the field names are correct.
We've changed every test but the first because we need to pass the stubbed Validator class to the User
class constructor. We always need to pass the $fields
array to the constructor as well, except in the one test that creates an empty user object. Otherwise, the validation won't happen.
Here's an example of one of our modified test methods:
/** @throws Exception */ public function testConstructorBadEmailOnly() { $validator = $this->make(Validator::class, array( 'validateEmail' => function () { return false; }, 'validateUsername' => function () { return true; }, 'validatePhone' => 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); }
First, we use $this->make(Validator::class)
to create a stub for the Validator
class. We've changed the Class from User4
to Validator
. By using dependency injection, we no longer need to create a stub of the user class. We'll be using the actual User4
class in our code, but with a stubbed Validator
class injected.
Next, we create a user object to test against, passing it the stubbed $validator
, and the $fields
variable we use for most of the tests.
Finally, we make our assertions, which are almost unchanged except that they use the real User
class rather than the stubbed one from the previous articles. We've added a third parameter to the assertEquals()
line that will display the array of errors if the assertEquals
test fails.
Notice that we no longer have to prevent the constructor from running or call it directly. By using dependency injection, we've made that unnecessary. Our code is lot cleaner and easier to understand. It's also more robust.
Coming Up
In the next article, we'll look at the dependency-injected version of the User
class.
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.