2. Writing Tests for PHPUnit

    1. The tests for a class go into a class ClassTest.

    2. ClassTest inherits (most of the time) from PHPUnit\Framework\TestCase.

    3. The tests are public methods that are named test*.

      Alternatively, you can use the @test annotation in a method’s docblock to mark it as a test method.

    4. Inside the test methods, assertion methods such as assertSame() (see Assertions) are used to assert that an actual value matches an expected value.

    Example 2.1 Testing array operations with PHPUnit

    Martin Fowler:

    Adrian Kuhn et. al.:

    Unit Tests are primarily written as a good practice to help developers identify and fix bugs, to refactor code and to serve as documentation for a unit of software under test. To achieve these benefits, unit tests ideally should cover all the possible paths in a program. One unit test usually covers one specific path in one function or method. However a test method is not necessarily an encapsulated, independent entity. Often there are implicit dependencies between test methods, hidden in the implementation scenario of a test.

    PHPUnit supports the declaration of explicit dependencies between test methods. Such dependencies do not define the order in which the test methods are to be executed but they allow the returning of an instance of the test fixture by a producer and passing it to the dependent consumers.

    • A producer is a test method that yields its unit under test as return value.

    • A consumer is a test method that depends on one or more producers and their return values.

    Example 2.2 shows how to use the @depends annotation to express dependencies between test methods.

    Example 2.2 Using the @depends annotation to express dependencies

    1. <?php declare(strict_types=1);
    2. use PHPUnit\Framework\TestCase;
    3. final class StackTest extends TestCase
    4. {
    5. public function testEmpty(): array
    6. {
    7. $stack = [];
    8. $this->assertEmpty($stack);
    9. return $stack;
    10. }
    11. /**
    12. * @depends testEmpty
    13. */
    14. public function testPush(array $stack): array
    15. {
    16. array_push($stack, 'foo');
    17. $this->assertSame('foo', $stack[count($stack)-1]);
    18. $this->assertNotEmpty($stack);
    19. return $stack;
    20. }
    21. /**
    22. * @depends testPush
    23. */
    24. public function testPop(array $stack): void
    25. {
    26. $this->assertSame('foo', array_pop($stack));
    27. $this->assertEmpty($stack);
    28. }
    29. }

    In the example above, the first test, testEmpty(), creates a new array and asserts that it is empty. The test then returns the fixture as its result. The second test, testPush(), depends on testEmpty() and is passed the result of that depended-upon test as its argument. Finally, testPop() depends upon testPush().

    Note

    The return value yielded by a producer is passed “as-is” to its consumers by default. This means that when a producer returns an object, a reference to that object is passed to the consumers. Instead of a reference either (a) a (deep) copy via @depends clone, or (b) a (normal shallow) clone (based on PHP keyword clone) via @depends shallowClone are possible too.

    To localize defects, we want our attention to be focussed on relevant failing tests. This is why PHPUnit skips the execution of a test when a depended-upon test has failed. This improves defect localization by exploiting the dependencies between tests as shown in .

    Example 2.3 Exploiting the dependencies between tests

    1. <?php declare(strict_types=1);
    2. use PHPUnit\Framework\TestCase;
    3. final class DependencyFailureTest extends TestCase
    4. {
    5. public function testOne(): void
    6. {
    7. $this->assertTrue(false);
    8. }
    9. /**
    10. * @depends testOne
    11. */
    12. public function testTwo(): void
    13. {
    14. }
    15. }
    1. $ phpunit --verbose DependencyFailureTest
    2. PHPUnit 9.6.0 by Sebastian Bergmann and contributors.
    3. FS
    4. Time: 0 seconds, Memory: 5.00Mb
    5. There was 1 failure:
    6. 1) DependencyFailureTest::testOne
    7. Failed asserting that false is true.
    8. /home/sb/DependencyFailureTest.php:6
    9. There was 1 skipped test:
    10. 1) DependencyFailureTest::testTwo
    11. This test depends on "DependencyFailureTest::testOne" to pass.
    12. FAILURES!
    13. Tests: 1, Assertions: 1, Failures: 1, Skipped: 1.

    A test may have more than one @depends annotation. PHPUnit does not change the order in which tests are executed, you have to ensure that the dependencies of a test can actually be met before the test is run.

    A test that has more than one @depends annotation will get a fixture from the first producer as the first argument, a fixture from the second producer as the second argument, and so on. See Example 2.4

    Example 2.4 Test with multiple dependencies

    1. <?php declare(strict_types=1);
    2. use PHPUnit\Framework\TestCase;
    3. final class MultipleDependenciesTest extends TestCase
    4. {
    5. public function testProducerFirst(): string
    6. {
    7. $this->assertTrue(true);
    8. return 'first';
    9. }
    10. public function testProducerSecond(): string
    11. {
    12. $this->assertTrue(true);
    13. return 'second';
    14. }
    15. /**
    16. * @depends testProducerFirst
    17. * @depends testProducerSecond
    18. */
    19. public function testConsumer(string $a, string $b): void
    20. {
    21. $this->assertSame('first', $a);
    22. $this->assertSame('second', $b);
    23. }
    24. }
    1. $ phpunit --verbose MultipleDependenciesTest
    2. PHPUnit 9.6.0 by Sebastian Bergmann and contributors.
    3. ...
    4. Time: 0 seconds, Memory: 3.25Mb
    5. OK (3 tests, 4 assertions)

    Data Providers

    A test method can accept arbitrary arguments. These arguments are to be provided by one or more data provider methods (additionProvider() in Example 2.5). The data provider method to be used is specified using the @dataProvider annotation.

    Example 2.5 Using a data provider that returns an array of arrays

    1. <?php declare(strict_types=1);
    2. use PHPUnit\Framework\TestCase;
    3. final class DataTest extends TestCase
    4. {
    5. /**
    6. * @dataProvider additionProvider
    7. */
    8. public function testAdd(int $a, int $b, int $expected): void
    9. {
    10. $this->assertSame($expected, $a + $b);
    11. }
    12. public function additionProvider(): array
    13. {
    14. return [
    15. [0, 0, 0],
    16. [0, 1, 1],
    17. [1, 0, 1],
    18. [1, 1, 3]
    19. ];
    20. }
    21. }
    1. $ phpunit DataTest
    2. PHPUnit 9.6.0 by Sebastian Bergmann and contributors.
    3. ...F
    4. Time: 0 seconds, Memory: 5.75Mb
    5. There was 1 failure:
    6. 1) DataTest::testAdd with data set #3 (1, 1, 3)
    7. Failed asserting that 2 is identical to 3.
    8. /home/sb/DataTest.php:9
    9. FAILURES!
    10. Tests: 4, Assertions: 4, Failures: 1.

    When using a large number of datasets it’s useful to name each one with string key instead of default numeric. Output will be more verbose as it’ll contain that name of a dataset that breaks a test.

    Example 2.6 Using a data provider with named datasets

    1. <?php declare(strict_types=1);
    2. use PHPUnit\Framework\TestCase;
    3. final class DataTest extends TestCase
    4. {
    5. /**
    6. * @dataProvider additionProvider
    7. */
    8. public function testAdd(int $a, int $b, int $expected): void
    9. {
    10. $this->assertSame($expected, $a + $b);
    11. }
    12. public function additionProvider(): array
    13. {
    14. return [
    15. 'adding zeros' => [0, 0, 0],
    16. 'zero plus one' => [0, 1, 1],
    17. 'one plus zero' => [1, 0, 1],
    18. 'one plus one' => [1, 1, 3]
    19. ];
    20. }
    21. }
    1. $ phpunit DataTest
    2. PHPUnit 9.6.0 by Sebastian Bergmann and contributors.
    3. ...F
    4. Time: 0 seconds, Memory: 5.75Mb
    5. There was 1 failure:
    6. 1) DataTest::testAdd with data set "one plus one" (1, 1, 3)
    7. Failed asserting that 2 is identical to 3.
    8. /home/sb/DataTest.php:9
    9. FAILURES!
    10. Tests: 4, Assertions: 4, Failures: 1.

    Note

    You can make the test output more verbose by defining a sentence and using the test’s parameter names as placeholders ($a, $b and $expected in the example above) with the annotation. You can also refer to the name of a named data set with $_dataName.

    Example 2.7 Using a data provider that returns an Iterator object

    1. $ phpunit DataTest
    2. PHPUnit 9.6.0 by Sebastian Bergmann and contributors.
    3. ...F
    4. Time: 0 seconds, Memory: 5.75Mb
    5. There was 1 failure:
    6. 1) DataTest::testAdd with data set #3 ('1', '1', '3')
    7. /home/sb/DataTest.php:11
    8. FAILURES!
    9. Tests: 4, Assertions: 4, Failures: 1.

    Example 2.8 The CsvFileIterator class

    1. <?php declare(strict_types=1);
    2. use PHPUnit\Framework\TestCase;
    3. final class CsvFileIterator implements Iterator
    4. {
    5. private $file;
    6. private $key = 0;
    7. private $current;
    8. public function __construct(string $file)
    9. {
    10. $this->file = fopen($file, 'r');
    11. }
    12. public function __destruct()
    13. {
    14. fclose($this->file);
    15. }
    16. public function rewind(): void
    17. {
    18. rewind($this->file);
    19. $this->current = fgetcsv($this->file);
    20. if (is_array($this->current)) {
    21. $this->current = array_map('intval', $this->current);
    22. }
    23. $this->key = 0;
    24. }
    25. public function valid(): bool
    26. {
    27. return !feof($this->file);
    28. }
    29. public function key(): int
    30. {
    31. return $this->key;
    32. }
    33. public function current(): array
    34. {
    35. return $this->current;
    36. }
    37. public function next(): void
    38. {
    39. $this->current = fgetcsv($this->file);
    40. if (is_array($this->current)) {
    41. $this->current = array_map('intval', $this->current);
    42. }
    43. $this->key++;
    44. }
    45. }

    When a test receives input from both a @dataProvider method and from one or more tests it @depends on, the arguments from the data provider will come before the ones from depended-upon tests. The arguments from depended-upon tests will be the same for each data set. See Example 2.9

    Example 2.9 Combination of @depends and @dataProvider in same test

    1. <?php declare(strict_types=1);
    2. use PHPUnit\Framework\TestCase;
    3. final class DependencyAndDataProviderComboTest extends TestCase
    4. {
    5. public function provider(): array
    6. {
    7. return [['provider1'], ['provider2']];
    8. }
    9. public function testProducerFirst(): string
    10. {
    11. $this->assertTrue(true);
    12. return 'first';
    13. }
    14. public function testProducerSecond(): string
    15. {
    16. $this->assertTrue(true);
    17. return 'second';
    18. }
    19. /**
    20. * @depends testProducerFirst
    21. * @depends testProducerSecond
    22. * @dataProvider provider
    23. */
    24. public function testConsumer(): void
    25. {
    26. $this->assertSame(
    27. ['provider1', 'first', 'second'],
    28. func_get_args()
    29. );
    30. }
    31. }
    1. $ phpunit --verbose DependencyAndDataProviderComboTest
    2. PHPUnit 9.6.0 by Sebastian Bergmann and contributors.
    3. ...F
    4. Time: 0 seconds, Memory: 3.50Mb
    5. There was 1 failure:
    6. 1) DependencyAndDataProviderComboTest::testConsumer with data set #1 ('provider2')
    7. Failed asserting that two arrays are identical.
    8. --- Expected
    9. +++ Actual
    10. @@ @@
    11. Array &0 (
    12. - 0 => 'provider1'
    13. + 0 => 'provider2'
    14. 1 => 'first'
    15. 2 => 'second'
    16. )
    17. /home/sb/DependencyAndDataProviderComboTest.php:32
    18. FAILURES!
    19. Tests: 4, Assertions: 4, Failures: 1.

    Example 2.10 Using multiple data providers for a single test

    1. <?php declare(strict_types=1);
    2. use PHPUnit\Framework\TestCase;
    3. final class DataTest extends TestCase
    4. {
    5. /**
    6. * @dataProvider additionWithNonNegativeNumbersProvider
    7. * @dataProvider additionWithNegativeNumbersProvider
    8. */
    9. public function testAdd(int $a, int $b, int $expected): void
    10. {
    11. $this->assertSame($expected, $a + $b);
    12. }
    13. public function additionWithNonNegativeNumbersProvider(): array
    14. {
    15. return [
    16. [0, 1, 1],
    17. [1, 0, 1],
    18. [1, 1, 3]
    19. ];
    20. }
    21. public function additionWithNegativeNumbersProvider(): array
    22. {
    23. return [
    24. [-1, 1, 0],
    25. [-1, -1, -2],
    26. [1, -1, 0]
    27. ];
    28. }
    29. }
    1. $ phpunit DataTest
    2. PHPUnit 9.6.0 by Sebastian Bergmann and contributors.
    3. ..F... 6 / 6 (100%)
    4. Time: 0 seconds, Memory: 5.75Mb
    5. There was 1 failure:
    6. 1) DataTest::testAdd with data set #3 (1, 1, 3)
    7. Failed asserting that 2 is identical to 3.
    8. /home/sb/DataTest.php:12
    9. FAILURES!
    10. Tests: 6, Assertions: 6, Failures: 1.

    Note

    When a test depends on a test that uses data providers, the depending test will be executed when the test it depends upon is successful for at least one data set. The result of a test that uses data providers cannot be injected into a depending test.

    Note

    All data providers are executed before both the call to the setUpBeforeClass() static method and the first call to the setUp() method. Because of that you can’t access any variables you create there from within a data provider. This is required in order for PHPUnit to be able to compute the total number of tests.

    Example 2.11 shows how to use the expectException() method to test whether an exception is thrown by the code under test.

    Example 2.11 Using the expectException() method

    1. <?php declare(strict_types=1);
    2. use PHPUnit\Framework\TestCase;
    3. final class ExceptionTest extends TestCase
    4. {
    5. public function testException(): void
    6. {
    7. $this->expectException(InvalidArgumentException::class);
    8. }
    9. }
    1. $ phpunit ExceptionTest
    2. PHPUnit 9.6.0 by Sebastian Bergmann and contributors.
    3. F
    4. Time: 0 seconds, Memory: 4.75Mb
    5. There was 1 failure:
    6. 1) ExceptionTest::testException
    7. Failed asserting that exception of type "InvalidArgumentException" is thrown.
    8. FAILURES!
    9. Tests: 1, Assertions: 1, Failures: 1.

    In addition to the expectException() method the expectExceptionCode(), expectExceptionMessage(), and expectExceptionMessageMatches() methods exist to set up expectations for exceptions raised by the code under test.

    Note

    Note that expectExceptionMessage() asserts that the $actual message contains the $expected message and does not perform an exact string comparison.

    Testing PHP Errors, Warnings, and Notices

    By default, PHPUnit converts PHP errors, warnings, and notices that are triggered during the execution of a test to an exception. Among other benefits, this makes it possible to expect that a PHP error, warning, or notice is triggered in a test as shown in Example 2.12.

    Note

    PHP’s error_reporting runtime configuration can limit which errors PHPUnit will convert to exceptions. If you are having issues with this feature, be sure PHP is not configured to suppress the type of error you are interested in.

    Example 2.12 Expecting PHP errors, warnings, and notices

    1. <?php declare(strict_types=1);
    2. use PHPUnit\Framework\TestCase;
    3. final class ErrorTest extends TestCase
    4. {
    5. public function testDeprecationCanBeExpected(): void
    6. {
    7. $this->expectDeprecation();
    8. // Optionally test that the message is equal to a string
    9. $this->expectDeprecationMessage('foo');
    10. // Or optionally test that the message matches a regular expression
    11. $this->expectDeprecationMessageMatches('/foo/');
    12. \trigger_error('foo', \E_USER_DEPRECATED);
    13. }
    14. public function testNoticeCanBeExpected(): void
    15. {
    16. $this->expectNotice();
    17. // Optionally test that the message is equal to a string
    18. $this->expectNoticeMessage('foo');
    19. // Or optionally test that the message matches a regular expression
    20. $this->expectNoticeMessageMatches('/foo/');
    21. \trigger_error('foo', \E_USER_NOTICE);
    22. }
    23. public function testWarningCanBeExpected(): void
    24. {
    25. $this->expectWarning();
    26. // Optionally test that the message is equal to a string
    27. $this->expectWarningMessage('foo');
    28. // Or optionally test that the message matches a regular expression
    29. $this->expectWarningMessageMatches('/foo/');
    30. \trigger_error('foo', \E_USER_WARNING);
    31. }
    32. public function testErrorCanBeExpected(): void
    33. {
    34. $this->expectError();
    35. // Optionally test that the message is equal to a string
    36. $this->expectErrorMessage('foo');
    37. // Or optionally test that the message matches a regular expression
    38. $this->expectErrorMessageMatches('/foo/');
    39. \trigger_error('foo', \E_USER_ERROR);
    40. }
    41. }

    When testing code that uses PHP built-in functions such as fopen() that may trigger errors it can sometimes be useful to use error suppression while testing. This allows you to check the return values by suppressing notices that would lead to an exception raised by PHPUnit’s error handler.

    1. $ phpunit ErrorSuppressionTest
    2. PHPUnit 9.6.0 by Sebastian Bergmann and contributors.
    3. .
    4. Time: 1 seconds, Memory: 5.25Mb
    5. OK (1 test, 1 assertion)

    Without the error suppression the test would fail reporting fopen(/is-not-writeable/file): failed to open stream: No such file or directory.

    Sometimes you want to assert that the execution of a method, for instance, generates an expected output (via echo or print, for example). The PHPUnit\Framework\TestCase class uses PHP’s Output Buffering feature to provide the functionality that is necessary for this.

    shows how to use the expectOutputString() method to set the expected output. If this expected output is not generated, the test will be counted as a failure.

    Example 2.14 Testing the output of a function or method

    1. <?php declare(strict_types=1);
    2. use PHPUnit\Framework\TestCase;
    3. final class OutputTest extends TestCase
    4. {
    5. public function testExpectFooActualFoo(): void
    6. {
    7. $this->expectOutputString('foo');
    8. print 'foo';
    9. }
    10. public function testExpectBarActualBaz(): void
    11. {
    12. $this->expectOutputString('bar');
    13. print 'baz';
    14. }
    15. }
    1. $ phpunit OutputTest
    2. PHPUnit 9.6.0 by Sebastian Bergmann and contributors.
    3. .F
    4. Time: 0 seconds, Memory: 5.75Mb
    5. There was 1 failure:
    6. 1) OutputTest::testExpectBarActualBaz
    7. Failed asserting that two strings are equal.
    8. --- Expected
    9. +++ Actual
    10. @@ @@
    11. -'bar'
    12. +'baz'
    13. FAILURES!
    14. Tests: 2, Assertions: 2, Failures: 1.

    Table 2.1 shows the methods provided for testing output

    Note

    A test that emits output will fail in strict mode.

    Error output

    Whenever a test fails PHPUnit tries its best to provide you with as much context as possible that can help to identify the problem.

    Example 2.15 Error output generated when an array comparison fails

    1. <?php declare(strict_types=1);
    2. use PHPUnit\Framework\TestCase;
    3. final class ArrayDiffTest extends TestCase
    4. {
    5. public function testEquality(): void
    6. {
    7. $this->assertSame(
    8. [1, 2, 3, 4, 5, 6],
    9. [1, 2, 33, 4, 5, 6]
    10. );
    11. }
    12. }
    1. $ phpunit ArrayDiffTest
    2. PHPUnit 9.6.0 by Sebastian Bergmann and contributors.
    3. F
    4. Time: 0 seconds, Memory: 5.25Mb
    5. There was 1 failure:
    6. 1) ArrayDiffTest::testEquality
    7. Failed asserting that two arrays are identical.
    8. --- Expected
    9. +++ Actual
    10. @@ @@
    11. Array (
    12. 0 => 1
    13. 1 => 2
    14. - 2 => 3
    15. + 2 => 33
    16. 3 => 4
    17. 4 => 5
    18. 5 => 6
    19. )
    20. /home/sb/ArrayDiffTest.php:7
    21. FAILURES!
    22. Tests: 1, Assertions: 1, Failures: 1.

    In this example only one of the array values differs and the other values are shown to provide context on where the error occurred.

    When the generated output would be long to read PHPUnit will split it up and provide a few lines of context around every difference.

    Example 2.16 Error output when an array comparison of a long array fails

    1. <?php declare(strict_types=1);
    2. use PHPUnit\Framework\TestCase;
    3. final class LongArrayDiffTest extends TestCase
    4. {
    5. public function testEquality(): void
    6. {
    7. $this->assertSame(
    8. [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 2, 3, 4, 5, 6],
    9. [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 2, 33, 4, 5, 6]
    10. );
    11. }
    12. }
    1. $ phpunit LongArrayDiffTest
    2. PHPUnit 9.6.0 by Sebastian Bergmann and contributors.
    3. F
    4. Time: 0 seconds, Memory: 5.25Mb
    5. There was 1 failure:
    6. 1) LongArrayDiffTest::testEquality
    7. Failed asserting that two arrays are identical.
    8. --- Expected
    9. +++ Actual
    10. @@ @@
    11. 11 => 0
    12. 12 => 1
    13. 13 => 2
    14. - 14 => 3
    15. + 14 => 33
    16. 15 => 4
    17. 16 => 5
    18. 17 => 6
    19. )
    20. /home/sb/LongArrayDiffTest.php:7
    21. FAILURES!
    22. Tests: 1, Assertions: 1, Failures: 1.

    When a comparison fails PHPUnit creates textual representations of the input values and compares those. Due to that implementation a diff might show more problems than actually exist.

    This only happens when using assertEquals() or other ‘weak’ comparison functions on arrays or objects.

    1. <?php declare(strict_types=1);
    2. use PHPUnit\Framework\TestCase;
    3. final class ArrayWeakComparisonTest extends TestCase
    4. {
    5. public function testEquality(): void
    6. {
    7. $this->assertEquals(
    8. [1, 2, 3, 4, 5, 6],
    9. ['1', 2, 33, 4, 5, 6]
    10. );
    11. }
    12. }
    1. $ phpunit ArrayWeakComparisonTest
    2. PHPUnit 9.6.0 by Sebastian Bergmann and contributors.
    3. F
    4. Time: 0 seconds, Memory: 5.25Mb
    5. There was 1 failure:
    6. 1) ArrayWeakComparisonTest::testEquality
    7. Failed asserting that two arrays are equal.
    8. --- Expected
    9. +++ Actual
    10. @@ @@
    11. Array (
    12. - 0 => 1
    13. + 0 => '1'
    14. 1 => 2
    15. - 2 => 3
    16. + 2 => 33
    17. 3 => 4
    18. 4 => 5
    19. 5 => 6
    20. )
    21. /home/sb/ArrayWeakComparisonTest.php:7
    22. Tests: 1, Assertions: 1, Failures: 1.

    In this example the difference in the first index between 1 and '1' is reported even though assertEquals() considers the values as a match.