Testing scalar type hints in PHP7 and PHPUnit

I have recently been playing with some of the new features in PHP 7. Although only in alpha release at present, I am pleasantly surprised at the platform stability and the backwards-compatibility which allows most of my existing code to run unmodified.

Scalar types and return types

The two features which most interest me are scalar type hints and return type declarations. I appreciate there are numerous schools of thought in the PHP community, but I am firmly in the camp which believes the following function is an abuse of at least two different languages:

<?php
function fingleTheFangles($fangle) {
    if (is_array($fangle)) {
        foreach ($fangle as $subfangle) {
            if (is_object($subfangle)) {
                // Do something objecty.
            } else if (is_string($subfangle)) {
                // Do something stringy.
            } else {
                // This must be numeric.
                // (it would obviously be FALSE to expect anything else!)
                // Do something numbery.
            }
            return $fingles.
        }
    } else {
        if (is_object($fangle)) {
            // Do something objecty.
        } else if (is_string($fangle)) {
            // Do something stringy.
        } else {
            // This must be numeric.
            // (it would obviously be FALSE to expect anything else!)
            // Do something numbery.
        }
        return $fingles.
    }
}

But now we could define the much more pleasing:

function fingleTheFangle(Fangle $fangle): Fingle {
    // Do something with a fangle.
    return $fingle;
}

function fingleTheFangleLabel(string $fangle_label): Fingle {
    // Do something with a string holding a fangle label.
    return $fingle;
}

function fingleTheFangleId(int $fangle_id): Fingle {
    // Do something with a numeric fangle id.
    return $fingle;
}

function fingleAnArrayOfFangles(array $fangles): array {
    // Do something with a whole array.
    return $fingles;
}

Testing scalar types

So how can we write unit tests to confirm our scalar hints are behaving as expected? Consider the following class:

<?php
declare(strict_types = 1);

class ScalarType {
    public function expectsInteger(int $integer) {
        // Do nothing
    }
}

If we call the function with an integer

expectsInteger(1)

it will execute as expected.

If instead we call the function with some other variable type (eg a string)

expectsInteger("not an integer")

then a TypeException is thrown by the PHP engine. So the following PHPUnit testcase should confirm this behaviour:

<?php
declare(strict_types = 1);

class ScalarTypeTest extends PHPUnit_Framework_TestCase {
    /**
     * @test
     * @expectedException TypeException
     */
    public function passStringWhenIntegerExpected() {
        $sut = new ScalarType();
        $sut->expectsInteger('Not an integer');
    }
}

It uses the PHPUnit annotation @expectedException to indicate the exception we expect to be thrown. Let’s try it out:

$ phpunit tests/ScalarTypeTest.php 
PHPUnit 4.7.3 by Sebastian Bergmann and contributors.

PHP Fatal error:  Uncaught TypeException: Argument 1 passed to ScalarType::expectsInteger() must be of the type integer, string given, called in .../PHPUnitTests/tests/ScalarTypeTest.php on line 11 and defined in .../PHPUnitTests/src/ScalarType.php:5
Stack trace:
#0 .../PHPUnitTests/tests/ScalarTypeTest.php(11): ScalarType->expectsInteger('Not an integer')
#1 [internal function]: ScalarTypeTest->passStringWhenIntegerExpected()
#2 .../PHPUnitTests/vendor/phpunit/phpunit/src/Framework/TestCase.php(863): ReflectionMethod->invokeArgs(Object(ScalarTypeTest), Array)
#3 .../PHPUnitTests/vendor/phpunit/phpunit/src/Framework/TestCase.php(741): PHPUnit_Framework_TestCase->runTest()
#4 .../PHPUnitTests/vendor/phpunit/phpunit/src/Framework/TestResult.php(605): PHPUnit_Framework_TestCase->runBare()
#5 .../PHPUnitTests/vendor/phpunit/phpunit/src/Framework/TestCa in .../PHPUnitTests/src/ScalarType.php on line 5

Fatal error: Uncaught TypeException: Argument 1 passed to ScalarType::expectsInteger() must be of the type integer, string given, called in .../PHPUnitTests/tests/ScalarTypeTest.php on line 11 and defined in .../PHPUnitTests/src/ScalarType.php:5
Stack trace:
#0 .../PHPUnitTests/tests/ScalarTypeTest.php(11): ScalarType->expectsInteger('Not an integer')
#1 [internal function]: ScalarTypeTest->passStringWhenIntegerExpected()
#2 .../PHPUnitTests/vendor/phpunit/phpunit/src/Framework/TestCase.php(863): ReflectionMethod->invokeArgs(Object(ScalarTypeTest), Array)
#3 .../PHPUnitTests/vendor/phpunit/phpunit/src/Framework/TestCase.php(741): PHPUnit_Framework_TestCase->runTest()
#4 .../PHPUnitTests/vendor/phpunit/phpunit/src/Framework/TestResult.php(605): PHPUnit_Framework_TestCase->runBare()
#5 .../PHPUnitTests/vendor/phpunit/phpunit/src/Framework/TestCa in .../PHPUnitTests/src/ScalarType.php on line 5

Okay… that’s not what we expected to happen. It looks like the TypeException was thrown, but PHPUnit has not caught the exception.

You see, another change in PHP 7 is the introduction of a new hierarchy of exceptions to replace Fatal Errors. Unfortunately for PHPUnit, these EngineExceptions do not derive from the Exception class, which is what PHPUnit attempts to catch. (In fact, Exception and EngineException now both derive from the BaseException class).

So if we want to test for this exception type, we’re going to have to catch it ourselves:

<?php
declare(strict_types = 1);

class ScalarTypeTest extends PHPUnit_Framework_TestCase {
    /**
     * @test
     */
    public function passStringWhenIntegerExpected() {
        $sut = new ScalarType();
        try {
            $sut->expectsInteger('Not an integer');
            $this->fail('Expected TypeException but none thrown.');
        } catch (TypeException $e) {
            // All good if we reach here.
        }
    }
}
$ phpunit tests/ScalarTypeTest.php 
PHPUnit 4.7.3 by Sebastian Bergmann and contributors.

.

Time: 421 ms, Memory: 4.00Mb

OK (1 test, 0 assertions)

I can only assume future releases of PHPUnit will be able to catch these exceptions too, but this workaround is hopefully not too painful in the short term.

Post to Twitter Post to Facebook