Basics of PHPUnit

Basic Test Class

PHPUnit test cases are written in classes which inherit all utilities like assertions, annotations, test doubles etc… from the core PHPUnit\\Framework\\TestCase class.

Note: Make sure you have same filename as the class

Eg:

<?php

class ExampleTestClass extends PHPUnit\\Framework\\TestCase {

function exampleTestCase() {
$this->assertTrue(<variable>);
}

}

Assertions

An assertion is a boolean expression at a specific point in a program which will be true unless there is a bug in the program. A test assertion is defined as an expression, which encapsulates some testable logic specified about a target under test.

In simple terms, an assertion is just a check which matches the result value to an expected value.

Eg:

<?php

use PHPUnit\\Framework\\TestCase;

class Test extends TestCase
{
    public function testSumPassing(): void
    {
        $a = 2;
        $b = 3;
       
        $result = $a + $b;
        $expected = 5;
       
        $this->assertEqual($expected, $result);
    }
   
    public function testSumFailing(): void
    {
        $a = 2;
        $b = 3;
       
        $result = $a + $b + 1;
        $expected = 5;
       
        $this->assertEqual($expected, $result);
    }
}

There are different assertion utitlities but here some which are most widely used.

  • assertArrayHasKey()
  • assertContains()
  • assertEmpty()
  • assertEquals()
  • assertIsArray()
  • assertNotNull()
  • assertSame()
  • assertTrue()

For further reference on assertions in PHPUnit you can visit: Assertions

Annotations

An annotation is a special form of syntactic metadata that can be added to the source code of some programming languages. In PHP we can use them using Reflection API’s getDocComment()

PHPUnit uses them to control the execution of tests. An example of annotation:

<?php declare(strict_types=1);
use PHPUnit\\Framework\\TestCase;

final class MyTest extends TestCase
{
    /**
    * @group specification
    */
    public function testSomething(): void
    {
    }

    /**
    * @group regression
    * @group bug2204
    */
    public function testSomethingElse(): void
    {
    }
}

You can run specific groups using phpunit test.php –group specification. Some commonly used annotations:

  • @group
  • @covers
  • @depends
  • @dataProvider

For further reference you can check: https://docs.phpunit.de/en/9.6/annotations.html

Fixtures

Fixtures of a test involve writing a code to set the world up in a known state, and after the test, return it back to its original state.

here setUp() and tearDown() are the methods used to setup Fixtures. They run before every test, thus configuring/initializing the fixture variable before and after every test.

<?php
use PHPUnit\\Framework\\TestCase;

class StackTest extends TestCase
{
    protected $stack;

    protected function setUp(): void
    {
        $this->stack = [];
    }

    public function testEmpty()
    {
        $this->assertTrue(empty($this->stack));
    }

    public function testPush()
    {
        array_push($this->stack, ‘foo’);
        $this->assertSame(‘foo’, $this->stack[count($this->stack)-1]);
        $this->assertFalse(empty($this->stack));
    }

    public function testPop()
    {
        array_push($this->stack, ‘foo’);
        $this->assertSame(‘foo’, array_pop($this->stack));
        $this->assertTrue(empty($this->stack));
    }
}

Example output:

Note: Please ignore the 3rd output, it is getting overwritten by the display function of PHPUnit

Data Providers

A data provider is a method which returns an array as the arguments for every test case in that class. The array row is spread as the arguments for test methods.

<?php
use PHPUnit\\Framework\\TestCase;

class DataProviderExampleTest extends TestCase
{  
    /**
    *
    * @dataProvider exampleDataProvider
    */
    public function testAdd($a, $b, $result) {
        $this->assertSame($result, $a + $b );
    }

    /**
    * Data provider method which returns an array as argument
    */
    public function exampleDataProvider(){
        return [
            [ 1, 2, 3 ], // Test 1
            [ 3, 6, 9 ], // Test 2
            [ 4, 3, 7 ]  // Test 3
        ];
    }
}

Test Doubles

When we are writing a test in which we cannot (or chose not to) use a real depended-on component (DOC), we can replace it with a Test Double. The Test Double doesn’t have to behave exactly like the real DOC; it merely has to provide the same API as the real one so that the System Under Test (SUT) thinks it is the real one!

Fakes

Fakes are objects that have working implementations. A fake object implements the same interface as a real object but takes shortcuts to improve performance. Fakes are generally used when we need to test something that depends on an external service or API, and we don’t want to make actual calls to that service.

Example: An in-memory database is fake because it implements the same interface as a real database but doesn’t use disk storage. It makes it much faster than a real database, but it also means that the data is only persisted in memory and will be lost when the application restarts.

Mocks

Mocks are objects that have predefined behavior. These objects register calls they receive, allowing us to assert how we use them in the code. Unlike fakes, mocks don’t have working implementations. Instead, they have pre-programmed expectations about how they will be used in the code.

Example: A mock object might be programmed to return a specific value when it is called with certain arguments. Mocks are generally used to test the behaviour of our code rather than its output. We can use mocks to verify that our code is calling the dependencies in an expected way.

Stubs

Stubs are objects that return predefined values. Like mocks, they don’t have working implementations. However, unlike mocks, they are not programmed to expect specific calls. Instead, they return values when they are called.

Example: A stub might be programmed to always return the same value when called with any arguments. Stubs are generally used to provide data that our code needs to run. This data can be hard-coded or generated dynamically.

Mock Example:

class SomeClass extends TestCase
{
public function someMethod() {
return ‘test’;
}
}

<?php
use PHPUnit\\Framework\\TestCase;

class TestDoubleExample extends TestCase
{
public function testStub() {
$stub = $this->createMock(SomeClass::class); // Will not instantiate main object

$stub->method(‘someMethod’)
->willReturn(‘foo’); // will return foo

$this->assertSame(‘foo’, $stub->someMethod());
}
}

As seen in the example, a stub is just a mock representation of a class which can be used as an object when testing a function in isolation without trying to initialize / instantiate that class.

You can read more about test doubles here: https://docs.phpunit.de/en/9.6/test-doubles.html

Code Coverage Analysis

In computer science, code coverage is a measure used to describe the degree to which the source code of a program is tested by a particular test suite. A program with high code coverage has been more thoroughly tested and has a lower chance of containing software bugs than a program with low code coverage.

For each test run, the PHPUnit command-line tool prints one character to indicate progress:

  • . – Printed when the test succeeds.
  • F – Printed when an assertion fails while running the test method.
  • E – Printed when an error occurs while running the test method.
  • R – Printed when the test has been marked as risky (see Risky Tests).
  • S – Printed when the test has been skipped (see Incomplete and Skipped Tests).
  • I – Printed when the test is marked as being incomplete or not yet implemented

Various software metrics exist to measure code coverage:

Line Coverage

The Line Coverage software metric measures whether each executable line was executed.

Branch Coverage

The Branch Coverage software metric measures whether the boolean expression of each control structure evaluated to both true and false while running the test suite.

Path Coverage

The Path Coverage software metric measures whether each of the possible execution paths in a function or method has been followed while running the test suite. An execution path is a unique sequence of branches from the entry of the function or method to its exit.

Function and Method Coverage

The Function and Method Coverage software metric measures whether each function or method has been invoked. php-code-coverage only considers a function or method as covered when all of its executable lines are covered.

Class and Trait Coverage

The Class and Trait Coverage software metric measures whether each method of a class or trait is covered. php-code-coverage only considers a class or trait as covered when all of its methods are covered.

Change Risk Anti-Patterns (CRAP) Index

The Change Risk Anti-Patterns (CRAP) Index is calculated based on the cyclomatic complexity and code coverage of a unit of code. Code that is not too complex and has an adequate test coverage will have a low CRAP index. The CRAP index can be lowered by writing tests and by refactoring the code to lower its complexity.

You can run code coverage by using:

  • phpunit <coverage-option>

Follow are the available options:

Code Coverage Options:
  –coverage-clover <file>    Generate code coverage report in Clover XML format
  –coverage-crap4j <file>    Generate code coverage report in Crap4J XML format
  –coverage-html <dir>       Generate code coverage report in HTML format
  –coverage-php <file>       Export PHP_CodeCoverage object to file
  –coverage-text=<file>      Generate code coverage report in text format [default: standard output]
  –coverage-xml <dir>        Generate code coverage report in PHPUnit XML format
  –coverage-cache <dir>      Cache static analysis results
  –warm-coverage-cache       Warm static analysis cache
  –coverage-filter <dir>     Include <dir> in code coverage analysis
  –path-coverage             Perform path coverage analysis
  –disable-coverage-ignore   Disable annotations for ignoring code coverage
  –no-coverage               Ignore code coverage configuration

For further reference you can visit:

Setting Expectations for tests

In order to test whether the API is used correctly or not, we have provided this small example.

It just gets the total and calculates the tax.

Here `getTotal` is a method which needs to be there. In this case we are not testing $Bill we are using Unit Testing just to check the implementation / code coverage

coverage

<?php
use PHPUnit\Framework\TestCase;

class Bill {
public function getTotal() {
return 200;
}
}

class Tax {
public function getTax(Bill $bill) {
$total = $bill->getTotal();
$tax = $total % 10;
return $tax;
}
}

class TestTax extends TestCase
{
public function testGetTax() {
$testee = new Tax();
$bill = $this->createMock(Bill::class);
$bill->expects($this->once())->method(‘getTotal’);

$testee->getTax($bill);
}
}

Explanation:

$bill->expects($this->once())->method(‘getTotal’);

expects() = tell PHPUnit to expect something a specified number of times

 $this->once()  = just once

If we want to expect it more than once, we can use 

 $this->exactly(2)  = 2 times,   $this->exactly(3) = 3 times, etc…

$bill->method(‘getTotal’);

This takes the method name as the argument and checks for the expectation passed before.