Unit Testing XIII - Inversion of Control and Dependency Injection

Using Inversion of Control (IOC) and Dependency Injection (DI), to improve your code


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.


MODX logo

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

  (Login)