Basic TDD in Your New PHP Package

Share this article

In part 1, we set up our development environment, baked in some rules as inherited from The League, and created two sample but useless classes – Diffbot and DiffbotException. In this part, we’ll get started with Test Driven Development.

If you’d like to follow along, please read Part 1 or clone the part 1 branch of this tutorial’s code.

PHPUnit

We’ve covered PHPUnit to some degree before (1, 2, 3, 4, 5, 6), but it’s time we put it into real practice now. First, let’s check if it’s installed.

php vendor/phpunit/phpunit/phpunit

Running this command should produce a report that says one test passed. This is the test included in the League Skeleton by default and it asserts that true is, in fact, true. A coverage report will also be generated and placed into the build folder.

If you open this coverage report in the browser, you should see we have a low coverage score.

Now that we’re sure PHPUnit works, let’s test something. Currently, we have little more than getters and setters in our class and those aren’t generally tested. So what can we test in our current code? Well.. how about the validity of the provided token through instantiation?

First, let’s see the PHPUnit XML configuration file, phpunit.xml.dist. After changing the word “League” to “Diffbot”, this is what it looks like:

<?xml version="1.0" encoding="UTF-8"?>
<phpunit bootstrap="vendor/autoload.php"
         backupGlobals="false"
         backupStaticAttributes="false"
         colors="true"
         verbose="true"
         convertErrorsToExceptions="true"
         convertNoticesToExceptions="true"
         convertWarningsToExceptions="true"
         processIsolation="false"
         stopOnFailure="false">
    <testsuites>
        <testsuite name="Diffbot Test Suite">
            <directory>tests</directory>
        </testsuite>
    </testsuites>
    <filter>
        <whitelist>
            <directory suffix=".php">src/</directory>
        </whitelist>
    </filter>
    <logging>
        <log type="tap" target="build/report.tap"/>
        <log type="junit" target="build/report.junit.xml"/>
        <log type="coverage-html" target="build/coverage" charset="UTF-8" yui="true" highlight="true"/>
        <log type="coverage-text" target="build/coverage.txt"/>
        <log type="coverage-clover" target="build/logs/clover.xml"/>
    </logging>
</phpunit>

The attributes of the main element tell PHPUnit to make its report as verbose as possible, and to convert all types of notices and errors to exceptions, along with some other typical defaults you can look into on their website. Then, we define testsuites – sets of tests applying to a given app or scenario. One such suite is the main application suite (the only one we’ll be using), and we call it the “Diffbot Test Suite”, defining the tests directory as the host of the tests – you’ll notice the sample League test is already inside that directory. We also tell PHPunit to ignore all PHP files in the src/ directory (we only want it to run tests, not our classes), and finally, we configure the logging – what it reports, how, and where to.

Let’s build our first test. In the tests folder, create DiffbotTest.php. If you’re using PhpStorm, this is almost automated:

Remember to check that the namespace in the composer.json matches this:

"autoload-dev": {
        "psr-4": {
            "Swader\\Diffbot\\Test\\": "tests/"
        }
    },

Feel free to delete the ExampleTest now (as well as the SkeletonClass), and replace the contents of our DiffbotTest class with the following:

<?php

namespace Swader\Diffbot\Test;

use Swader\Diffbot\Diffbot;
use Swader\Diffbot\Exceptions\DiffbotException;

/**
 * @runTestsInSeparateProcesses
 */
class DiffbotTest extends \PHPUnit_Framework_TestCase
{

    public function invalidTokens()
    {
        return [
            'empty'        => [ '' ],
            'a'            => [ 'a' ],
            'ab'           => [ 'ab' ],
            'abc'          => [ 'abc' ],
            'digit'        => [ 1 ],
            'double-digit' => [ 12 ],
            'triple-digit' => [ 123 ],
            'bool'         => [ true ],
            'array'        => [ ['token'] ],
        ];
    }

    public function validTokens()
    {
        return [
            'token'      => [ 'token' ],
            'short-hash' => [ '123456789' ],
            'full-hash'  => [ 'akrwejhtn983z420qrzc8397r4' ],
        ];
    }

    /**
     * @dataProvider invalidTokens
     */
    public function testSetTokenRaisesExceptionOnInvalidToken($token)
    {
        $this->setExpectedException('InvalidArgumentException');
        Diffbot::setToken($token);
    }

    /**
     * @dataProvider validTokens
     */
    public function testSetTokenSucceedsOnValidToken($token)
    {
        Diffbot::setToken($token);
        $bot = new Diffbot();
        $this->assertInstanceOf('\Swader\Diffbot\Diffbot', $bot);
    }
}

In this extremely simple example, we test the Diffbot::setToken static method. We use PHPUnit’s DataProvider syntax to feed the values in a loop automatically (many thanks to Matthew Weier O’Phinney for correcting my course in this). This also lets us know which of the keys failed when testing, rather than just expecting or not expecting an exception. If we now run the test and look at the coverage, we should see something like this:

The achievement addict in me triggers, and suddenly I want to see as much green as possible. Let’s test instantiation:

public function testInstantiationWithNoGlobalTokenAndNoArgumentRaisesAnException()
    {
        $this->setExpectedException('\Swader\Diffbot\Exceptions\DiffbotException');
        new Diffbot();
    }

    public function testInstantiationWithGlobalTokenAndNoArgumentSucceeds()
    {
        Diffbot::setToken('token');
        $bot = new Diffbot();
        $this->assertInstanceOf('Swader\Diffbot\Diffbot', $bot);
    }

    public function testInstantiationWithNoGlobalTokenButWithArgumentSucceeds()
    {
        $bot = new Diffbot('token');
        $this->assertInstanceOf('Swader\Diffbot\Diffbot', $bot);
    }

These methods cover all the cases of the constructor – instantiation without a token and without a globally set one, instantiation with a globally set one, and instantiation with just the token param. However, running the test, we’ll see it fails. This happens because the Diffbot class still has the static property set from the previous test, ergo not lacking the default static token when used in the second test. This is a common problem with testing globals and statics. To get around this, we’ll make sure each of our tests in the DiffbotTest class is executed in a separate process. This will be much slower to execute, but will make sure each environment is fresh and unpolluted.

Add the following annotation above the class declaraiton, like so:

/**
 * @runTestsInSeparateProcesses
 */
class DiffbotTest extends \PHPUnit_Framework_TestCase

Now if you run the test and look at the coverage, you’ll notice we’re 100% green!

This is an anti-pattern of sorts, and usually indicative of something being wrong with the design of the class if it needs separate processes for testing, but I’ve yet to find a better approach to test this. The static property in the Diffbot class should be mutable for ease of use – if you have suggestions on improving this, I’m all ears. An alternative approach to solving this problem is building a reset method or some additional setters that you can use to manually return the class to its original state, but I avoid this approach in order not to pollute my class with test-related logic. Word is, this can be solved with backupStaticAttributes, too, but I’ve failed to make it work so far.

TDD

In TDD, you’re generally supposed to think of functionality, then test for it (and fail) and then implement it so it works. That’s where the testing drives your development, hence, test driven development. This is exactly what we’ll do in this section.

Diffbot, as a service, offers several default APIs:

  • Article API extracts structured data from article type content like news and blog posts
  • Product API extracts info about products. Send it to a product link and it’ll pull price, availability, specs, and more.
  • Image API gets you info about an image, or a set of images if you pass it a link to a page with several
  • Analyze API automatically determines which of the above three APIs to use, and auto applies it. It tries to use the approach that produces the most information when given a URL.
  • Video and Discussion APIs are still in development. Video is the same as Image API but for video files, while Discussion can extract conversation threads from forums, comment sections on various sites and social network posts, and more.

As evident by the documentation, each of the APIs returns a similar response (all return valid JSON), but the fields returned mostly differ. This is how I see the Diffbot class as a final product – it has methods for each API type, and each API type is a separate class we’re yet to develop. These API classes all extend one abstract API class which contains the setters for the common fields, but each API class itself contains its own settable fields, too. In a nutshell, I’d like to make the following approaches possible:

$diffbot = new Diffbot('myToken');

$productAPI = $diffbot->createProductAPI('http://someurl.com');
$productAPI
    ->setTimeout(3000)
    ->setFields(['prefixCode', 'productOrigin']);
$response = $productAPI->call();

// OR, LIKE THIS

$response = $diffbot
    ->createProductAPI('http://someurl.com')
    ->setTimeout(0)
    ->setPrefixCode(true)
    ->setProductOrigin(true)
    ->setHeaderCookie(['key' => 'value', 'key2' => 'value2'])
    ->call();

Testing Abstract Classes

To make the API subclasses, we’ll need a common API abstract class which to extend. But how do we test abstract classes without extending them? With Test Doubles. As you probably know, you can’t instantiate an abstract class on its own – it needs to be extended. Hence, if an abstract class can’t be instantiated, there’s no way to test its concrete methods – those inherited by all sub-classes. A test double can be used to make a fake version of an extended abstract class, used to then test only the abstract class’ concrete methods. It’s best to show you on an example. Let’s assume our API abstract will have a method setTimeout used to set the API request timeout on the Diffbot side. Let’s also assume that any number from 0 to max int is valid. In a true TDD fashion, let’s make the file tests/Abstracts/ApiTest.php with the content:

<?php

namespace Swader\Diffbot\Test;

use Swader\Diffbot\Abstracts\Api;

class ApiTest extends \PHPUnit_Framework_TestCase
{
    /**
     * @return \PHPUnit_Framework_MockObject_MockObject
     */
    private function buildMock()
    {
        return $this->getMockForAbstractClass('Swader\Diffbot\Abstracts\Api');
    }

    public function validTimeouts()
    {
        return [
            'zero' => [0],
            '1000' => [1000],
            '2000' => [2000],
            '3000' => [3000],
            '3 mil' => [3000000],
            '40 mil' => [40000000]
        ];
    }

    public function invalidTimeouts()
    {
        return [
            'negative_big' => [-298979879827],
            'negative_small' => [-4983],
            'string ' => ['abcef'],
            'empty string' => [''],
            'bool' => [false]
        ];
    }

    public function testSetEmptyTimeoutSuccess()
    {
        /** @var Api $mock */
        $mock = $this->buildMock();
        $mock->setTimeout();
    }

    /**
     * @dataProvider invalidTimeouts
     */
    public function testSetTimeoutInvalid($timeout)
    {
        /** @var Api $mock */
        $mock = $this->buildMock();
        $this->setExpectedException('InvalidArgumentException');
        $mock->setTimeout($timeout);
    }

    /**
     * @dataProvider validTimeouts
     */
    public function testSetTimeoutValid($timeout)
    {
        /** @var Api $mock */
        $mock = $this->buildMock();
        $mock->setTimeout($timeout);
    }
}

We define some data providers just like in the first test class. Then, we have a method for creating our mock, so we can call it when needed. Next, we make a test method for each scenario – no timeout argument, bad argument and good argument. Only the bad one expects and exception.

If we run this test now, we’ll get an error:

This is not at all surprising – after all, we haven’t added the API class yet! Create the file src/Abstracts/Api.php with the content:

<?php

namespace Swader\Diffbot\Abstracts;

/**
 * Class Api
 * @package Swader\Diffbot\Abstracts
 */
abstract class Api
{
    
}

Running the test now produces a new error:

Whoa! We broke PHPUnit! Just kidding, we’re good. It’s complaining about not having a setTimeout() method, which is expected in the test – the mock is supposed to have it. Let’s change Api.php.

<?php

namespace Swader\Diffbot\Abstracts;

/**
 * Class Api
 * @package Swader\Diffbot\Abstracts
 */
abstract class Api
{
    /** @var int Timeout value in ms - defaults to 30s if empty */
    private $timeout = 30000;

    public function setTimeout($timeout = null)
    {
        $this->timeout = $timeout;
    }
}

Re-running the test, we get:

Now we’re getting somewhere. Let’s make one final implementation of our desired functionality. We edit the body of the setTimeout method, like so:

/**
     * Setting the timeout will define how long Diffbot will keep trying
     * to fetch the API results. A timeout can happen for various reasons, from
     * Diffbot's failure, to the site being crawled being exceptionally slow, and more.
     * 
     * @param int|null $timeout Defaults to 30000 even if not set
     *
     * @return $this
     */
    public function setTimeout($timeout = null)
    {
        if ($timeout === null) {
            $timeout = 30000;
        }
        if (!is_int($timeout)) {
            throw new \InvalidArgumentException('Parameter is not an integer');
        }
        if ($timeout < 0) {
            throw new \InvalidArgumentException('Parameter is negative. Only positive timeouts accepted.');
        }

        $this->timeout = $timeout;
        return $this;
    }

Along with logic, we added a docblock, and made the function return the instance of the class we’re using, so we can chain methods. Re-running the tests, all should pass. In fact, if we look at the coverage report, we should be 100% green.

Conclusion

In part 2, we started our TDD adventure by introducing PHPUnit and using it to develop some of our package’s functionality. You can download the full code of part 2 (includes part 1 code) from this branch. In the next part, we’ll continue building the package using the methods described here, and we’ll add in a new aspect – data mocking. Stay tuned!

Bruno SkvorcBruno Skvorc
View Author

Bruno is a blockchain developer and technical educator at the Web3 Foundation, the foundation that's building the next generation of the free people's internet. He runs two newsletters you should subscribe to if you're interested in Web3.0: Dot Leap covers ecosystem and tech development of Web3, and NFT Review covers the evolution of the non-fungible token (digital collectibles) ecosystem inside this emerging new web. His current passion project is RMRK.app, the most advanced NFT system in the world, which allows NFTs to own other NFTs, NFTs to react to emotion, NFTs to be governed democratically, and NFTs to be multiple things at once.

BrunoSOOPHPPHPphp packagephp5php7phpunittdd
Share this article
Read Next
Get the freshest news and resources for developers, designers and digital creators in your inbox each week