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 EngineException
s 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.