Getting Started

Installing PHPUnit via PHPQA

There are two ways to install PHPUnit. The conventional way is to add it as a direct dependency, but we will focus on using PHPUnit as part of the PHPQA Library.

See the PHPQA Installation instructions to add the relevant Composer dependencies.

Testing Basics

Writing your first test

As you've seen earlier, tests are named according to the Application class they're testing.

So for the Addition class, we can create a simple test to make sure adding two numbers comes back with what we expect.

You can see this test in App\Tests\Small\Maths\AdditionTest

When you run PHPUnit against this file, you'll see that it passes the test:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
16:19 $ bin/phpunit tests/Small/Maths/AdditionTest.php 

...

App\Tests\Small\Maths\Addition
 ✔ Adding numbers works [14.45 ms]

Time: 149 ms, Memory: 4.00MB

OK (1 test, 1 assertion)

...

Assertion types

There are a lot of test types offered by PHPUnit\Framework\TestCase. Some key ones are:

  • assertSame($expected, $actual) - Check that one value matches another exactly. There's also assertEquals, which doesn't check the types
  • assertInstanceOf($expected, $actual) - check that the object is of an expected type
  • assertTrue($condition) - check that the passed value is true
  • assertContains($needle, $haystack) - check either that a string contains another string, or that an array contains an item
  • assertCount($expectedCount, $haystack) - check that an array contains a specified number of items
  • expectException($class) - Add before a method call to test that the exception is thrown

A few more pointers:

  • Many of these are negatable such as assertNotSame().
  • Many of them have an additional third parameter $message for what to display when the test fails.
  • You may use multiple assertions within one test method

An example of these is documented in a new App\Tests\Small\Maths\DivisionTest

More Advanced Testing

Data Providers

If you have a test method you want to run multiple times but with a few different values you can use Data Providers. The Data Provider is a method within the same Test class.

  • The method name should be prefixed with provides
  • This method returns an associative array
    • The key is used when reporting if a test passes or fails ("data set Num 5")
    • The array's values are passed into the test method in order of test method parameters
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
    public function providesPrimeNumbers(): array
    {
            // Collect a list of known prime numbers
            return [
                'Num 5' => [5],
                'Num 7' => [7],
                'Num 11' => [11],
                ...
            ];
        }
    }

When setting up your test method you would add method arguments, which will then have values fed in by the Data Provider

You specify which method is to be used as the data provider using the @dataProvider annotation

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
    /**
     * @test
     * @dataProvider providesPrimeNumbers
     */
    public function itCanDetectIfPrimeNumber(int $number) : void
    {
        // Test that the Prime Number helper correctly identifies the passed $number as a Prime Number
        $isPrimeNumber = $this->helper::isPrimeNumbers($number);

        self::assertTrue($isPrimeNumber);
    }

An example of using a data provider is at App\Tests\Small\Maths\PrimeNumbersTest::providesPrimeNumbers()

PHPUnit Data Providers documentation

Test Dependencies

Tests can depend on other methods within the same test class using the @depends annotation

The following shows two individual tests, with the second depending on the first.

When a test depends on another, the return value of the dependency is passed into the test function as an argument.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
    /**
    * @test
    */
    public function canCreateProduct() : Product
    {
        $product = ProductManager::createProduct();
        $this->assertInstanceOf(Product, $product)
        return $product;
    }

    /**
    * @test
    * @depends canCreateProduct
    */
    public function canAddData(Product $product)
    {
        $product->addData('key', 'value');
        ...

If you create a third test, canStoreProduct() which @depends on canAddData() it will:

Test Assets, Fakes, Stubs, Mocks

For anything that is not an actual test, you should put this in an Assets folder

Inside this folder, you can sub folder as you see fit

It would make sense to respect the tested code folder structure for organisation but this is not a requirement.

Life Cycle Methods

setUpBeforeClass and tearDownAfterClass

In some cases, multiple tests within one test class can have expensive operations. You can avoid running these operations multiple times by adding a method named setUp() and tearDown(). PHPUnit will run these only once per Test Class before and after running the tests respectively. You should make sure this does not predispose the tests to an artificially set up environment, which passes tests which would fail in a clean environment.

setUp() and tearDown() are nicely symmetrical in theory but not in practice. In practice, you only need to implement tearDown() if you have allocated external resources like files or sockets in setUp(). If your setUp() just creates plain PHP objects, you can generally ignore tearDown(). However, if you create many objects in your setUp(), you might want to unset() the variables pointing to those objects in your tearDown() so they can be garbage collected. The garbage collection of test case objects is not predictable.

SetUp and TearDown

These are similar to the -beforeClass and -afterClass methods, but insead are run on a per-method basis.

An example of using a setUp method is at App\Tests\Small\Maths\PrimeNumbersTest::setup()

assertPreConditions() and assertPostConditions()

These are run before and after every method of the test case class but after setUp() and before tearDown() respectively. In the case of assertPostConditions(), it will run only if the test method did not fail.

assertPreConditions() and assertPostConditions() are two methods executed before and after the a test method. They differ from setUp() and tearDown() since they are executed only if the test did not already fail and they can halt the execution with a failing assertion. setUp() and tearDown() should never throw exceptions and they are executed anyway, unconcerned by the current test outcome.

Lifecycle Example

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
$ phpunit TemplateMethodsTest
PHPUnit |version|.0 by Sebastian Bergmann and contributors.

TemplateMethodsTest::setUpBeforeClass
TemplateMethodsTest::setUp
TemplateMethodsTest::assertPreConditions
TemplateMethodsTest::testOne
TemplateMethodsTest::assertPostConditions
TemplateMethodsTest::tearDown
.TemplateMethodsTest::setUp
TemplateMethodsTest::assertPreConditions
TemplateMethodsTest::testTwo
TemplateMethodsTest::tearDown
TemplateMethodsTest::onNotSuccessfulTest
FTemplateMethodsTest::tearDownAfterClass

Time: 0 seconds, Memory: 5.25Mb

There was 1 failure:

1) TemplateMethodsTest::testTwo
Failed asserting that <boolean:false> is true.
/home/sb/TemplateMethodsTest.php:30

FAILURES!
Tests: 2, Assertions: 2, Failures: 1.

Test Driven Development

One core principle is that the application is not run manually, but instead always run through your Unit Tests.

In theory this means writing your tests before you even start writing your application code, but in reality this often isn't the case. You will need to bear in mind that your code will need to be run using a Unit Test.

An example of this is that the Addition and Division classes' methods accept only int values. There's no reason these can't be floats instead.

This is where TDD comes in - you should first write new tests which pass in float values. The existing tests will pass since floats are valid ints. Before changing the application code you should write new tests which pass in float values. These will initially fail. You then change your code until these new tests pass.

Further Reading