Unit Testing IX - Testing a Validator Class

Creating a test for a separate validator class using a data provider


In the previous article in this series, we looked are our new User class with separate methods for validating usernames, email addresses, and phone numbers. In this one, we'll start working on a Validator class that encapsulates those methods. In keeping with our Test-driven development (TDD) theme, we'll create the tests first, then we'll create the class in the following article. We'll also see how to use a data provider to supply test cases for our tests.


MODX logo

Validator Test

Creating a test of our Validator class will allow us to test the validation code without involving the User class at all.

To test our Validator class, we'll need to supply lots of valid and invalid values to our validators. We need to make sure they correctly identify good and bad values. This will also let us test various ways to validate things.

For example, you might see a regular expression that's much shorter than your current one and claims to validate email addresses for 99% of all cases. You can try it in our unit test to see if that claim is realistic.

In an earlier article, we created class variables in our unit test class containing valid and invalid values for the username, email, and phone fields of our user object. When you have a lengthy list of values, it's more appropriate to use a data provider.


Data Providers

A data provider is simply a method that lets you iterate through a data set in your tests. Here's a basic example:

    /**
     * @dataProvider additionProvider
     * @param int $value1
     * @param int $value2
     * @param int $result
     */
    public function testAdd($value1, $value2, $result) {
        $sum = $this->_add($value1, $value2);

        assertEquals($result, $sum);
    }

    public function _add($v1, $v2) {
        return $v1 + $v2;
    }

    public function additionProvider() {
        /* Test Cases to validate addition */

        return array(
            array(1, 2, 3),
            array(2, 3, 5),
            array(2, 2, 4),
            array(7,1,8),
        );
    }

Let's start at the bottom. The additionProvider() method simply returns a nested array where each row is a test case. The first and second numbers are the numbers to add. The third one is the expected result. The name of this method is arbitrary, but must match the name given in the comment ahead of any test method that uses this data provider.

Above that, we have the _add() method, which takes two integer parameters and returns their sum. Normally, this method would be in the class you are testing (say, a calculator class). I put the method here for convenience. The underscore at the beginning of the method name indicates that the method is not a test method. This is handy any time you have a function that's used by one or more of your actual test methods but shouldn't be considered a test. PhpUnit and Codeception will ignore it unless it is called directly by a test method. In other words, it won't be run as a test.

At the top, we have the PhpDoc comment that sets things up:

/**
     * @dataProvider additionProvider
     * @param int $value1
     * @param int $value2
     * @param int $result
     */

Note the double asterisks (/**) at the beginning of the comment tag. These are critical. Without them Codeception will not recognize this as a "special" comment and will not set up the data provider. When you run the test you'll get a complaint that too few parameters were passed to the test method.

The @dataProvider additionProvider line (required) just gives the name of our data provider method.

After that are the three parameters used by our test method and their types. This section is optional, but it's good form because it helps explain what's happening in the test to anyone reading the code.

Just below the comment, we have the test method:

public function testAdd($value1, $value2, $result) {
    $sum = $this->_add($value1, $value2);
    assertEquals($result, $sum);
}

It looks like our test has only one assertion, but if you run it you'll see that there are four of them. Because we've specified a data provider, the test will be run once for each line of the array returned by the data provider.


Validator Test

Back to testing our Validator class. Here's the code:

<?php
class ValidatorTest extends \Codeception\Test\Unit
{
    /** @var \UnitTester */
    /** @var Validator $validator */
    protected $validator;

    protected function _before() {
        require_once 'C:\xampp\htdocs\addons\assets\mycomponents\testingproject\core\model\validator.class.php';
        $this->validator = new Validator();
    }


    protected function _after()
    {}

    /**
     * @dataProvider usernameProvider
     * @param string $username
     * @param bool $expected
     */
    public function testUsernameValidator($username, $expected) {
        assertEquals($expected, $this->validator->validateUsername($username), $username);
    }

    /**
     * @dataProvider emailProvider
     * @param string $email
     * @param bool $expected
     */
    public function testEmailValidator($email, $expected) {
        assertEquals($expected, $this->validator->validateEmail($email), $email);
    }

    /**
     * @dataProvider phoneProvider
     * @param string $phone
     * @param bool $expected
     */
    public function testPhoneValidator($phone, $expected) {
        assertEquals($expected, $this->validator->validatePhone($phone), $phone);
    }

    public function usernameProvider() {
        return array(
            /* Valid */
            array('Bob', true),
            array('CaptainMarvel', true),

            /* Invalid */
            array('Bo', false),
            array('asdljkasdlkjasdlkjasldkjasl', false),
        );
    }

    public function emailProvider() {
        /* Test Cases for validating email address */
        return array(
            /* Valid */
            array('simple@example.com', true),
            array('very.common@example.com', true),
            array('disposable.style.email.with+symbol@example.com', true),
            array('other.email-with-dash@example.com', true),
            array('fully-qualified-domain@example.com', true),
            array('x@example.com' , true),
            array('"very.unusual.@.unusual.com"@example.com', true),
            array('"very.(),:;<>[]\".VERY.\"very@\ \"very\".unusual"@strange.example.com', true),
            array('example-indeed@strange-example.com', true),
            array('/#!$%&\'*+-/=?^_`{}|~@example.org', true),
            array('example@s.solutions', true),
            array('user@[IPv6:2001:DB8::1]', true),

            /* Invalid */
            array('', false),
            array('Abc.example.com', false),
            array('A@b@c@example.com', false),
            array('a"b(c)d,e:f;gi[j\k]l@example.com', false),
            array('just"not"right@example.com', false),
            array('this is"not\allowed@example.com', false),
            array('this\ still\"not\allowed@example.com', false),
            array('1234567890123456789012345678901234567890123456789012345678901234+x@example.com',                   false),
            array('john..doe@example.com', false),
            array('john.doe@example..com', false),
            array('"much.more unusual"@example.com', false),
            array(' very.common@example.com', false),
            array('very.common@example.com ', false),
         );
    }

    public function phoneProvider() {
        /* Test Cases to validate
            USA phone number*/
        return array(
            /* valid */
            array('1-234-567-8901', true),
            array('+1-234-567-8901', true),
            array('651-321-1345', true),
            array('(651) 321-1345', true),
            array('1-234-567-8901 x1234', true),
            array('1-234-567-8901 ext1234', true),
            array('1-234-567-8901 extension 1234', true),
            array('1 (234) 567-8901', true),
            array('1.234.567.8901', true),
            array('12345678901', true),
            array('2345678901', true),

            /* Invalid */
            array('12345', false),
            array('1', false),
            array('123456', false),
            array('(213.123-4567', false),
            array('(xxx) xxx-xxxx', false),
            array('(123) 123-12345', false),
            array('(123) 1233-1234', false),
            array('(1234) 123-1234', false),
            array('911', false),
            array('411', false),
        );
    }
}

To create this test, issue this command while in the _build directory:

codecept generate:test unit Validator

The file ValidatorTest.php will be created in the test/unit/ directory.

The code for this test is available at GitHub here.

In the code above, there are three data providers, one for each field the validator can check. All three contain rows of data, which each have two members. The first is the value to check. The second is a True/False flag indicating whether the value is valid or invalid.

The tests methods themselves are very short. The PhpDoc comment is actually longer than the test class. The actual test code is only one line. Here's the username validator test:

 /**
     * @dataProvider usernameProvider
     * @param string $username
     * @param bool $expected
     */
    public function testUsernameValidator($username, $expected) {
        assertEquals($expected, $this->validator->validateUsername($username), $username);
    }
 

The assertion will run four times — once for each line of the array returned by the username data provider method. In each assert, the $expected variable will hold the second member of the line's array (because it's the second parameter to the test method). The $username variable will hold first parameter to the method: the username we're validating.

By setting things up this way, we're able to test both valid and invalid values in the same test.

Notice that we've added a third parameter to the call to validateUsername. This is the message that will appear in the console if a test fails. In this case, it will display the actual username that was not validated properly. In other words, you'll see the username if it was expected to be valid but was found invalid by the validator method, or vice versa.

The other two test methods (validateEmail() and validatePhone()) work the same way this one does. In this test file, all the data providers return data in the same format, with two data members in each line of the array returned by the provider, but this doesn't have to be the case. The data provider methods are all independent, and each one can have the lines of the data array contain as few or as many members as you need for your test as long as there are the same number of parameters in the test method.


Coming Up

As expected, this test won't run until we create the Validator class file referenced in the before() method. We'll see that file in the next article.

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



For more information on how to use MODX to create a website, see my website 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)