Get up to 80 % extra points for free! More info:

Lesson 3 - PHP Testing - Finishing Unit Tests

In the previous lesson, Introduction to unit tests in PHP and PHPUnit installation, we installed the Codeception framework and generated our first PHPUnit test. Today, we'll cover our simple class with tests, look at available assertion methods, and complete our unit tests in PHP.

Coverage of the class with tests

The _before() (previously setUp()) and _after() (previously tearDown()) methods are called before, resp. after every test in this class. This is very important for us because, according to best practices, we want the tests to be independent. Usually, before each test, we are preparing the same environment again, so that they don't affect each other at all. Good practices will be discussed later in detail. Add the $calculator attribute to the class, and in the _before() method, we always create a new calculator instance for each test. If it would need more to be set up or some additional dependencies were needed, it would also be in this method. We'll remove the testSomeFeature() method:

class CalculatorTest extends \Codeception\Test\Unit
{
    /**
     * @var \UnitTester
     */
    protected $tester;

    private $calculator;

    protected function _before()
    {
        $this->calculator = new Calculator();
    }

    protected function _after()
    {
    }

}

We have everything ready to add the tests themselves. Individual methods will always start with the "test" prefix and will test one particular method from the Calculator class, typically with several different inputs. If you're wondering why we mark the methods with prefixes, it help us to create the assistive methods that we can use in the test and won't be considered as tests. PhpStorm automatically triggers tests (methods starting with "test") and prints out their results.

Let's add the following 5 methods:

public function testSummation()
{
    $this->assertEquals(2, $this->calculator->sum(1, 1));
    $this->assertEquals(1.42, $this->calculator->sum(3.14, -1.72), '', 0.001);
    $this->assertEquals(2/3, $this->calculator->sum(1/3, 1/3), '', 0.001);
}

public function testSubtraction()
{
    $this->assertEquals(0, $this->calculator->subtract(1, 1));
    $this->assertEquals(4.86, $this->calculator->subtract(3.14, -1.72), '', 0.001);
    $this->assertEquals(2/3, $this->calculator->subtract(1/3, -1/3), '', 0.001);
}

public function testMultiplication()
{
    $this->assertEquals(2, $this->calculator->multiply(1, 2));
    $this->assertEquals(-5.4008, $this->calculator->multiply(3.14, -1.72), '', 0.001);
    $this->assertEquals(0.111, $this->calculator->multiply(1/3, 1/3), '', 0.001);
}

public function testDivision()
{
    $this->assertEquals(2, $this->calculator->divide(4, 2));
    $this->assertEquals(-1.826, $this->calculator->divide(3.14, -1.72), '', 0.001);
    $this->assertEquals(1, $this->calculator->divide(1/3, 1/3));
}

/**
 * @expectedException InvalidArgumentException
 */
public function testDivisionException()
{
    $this->calculator->divide(2, 0);
}

To compare the output of the method with the expected value, we use inherited assert* method. They can be called statically, but we'll stay with usage of instance. Most often you'll use assertEquals(), which accepts the expected result value as the first parameter and the current result value as the second parameter. This order is good to follow, otherwise you'll have the values ​​of the test results reversed. As you probably know, the decimal numbers are represented in the computer memory binary (how else :) ) and this causes some loss of their accuracy and some difficulties in comparing them. Therefore, we have to enter the fourth parameter in this case, and it is delta, a positive tolerance about, how much the expected and actual value can vary to keep the test successful. The third parameter is an error message in the case of the test failure. Usually there is no reason to enter it if the test is well named and we can simply recognize which assert call failed.

Note that we are trying different inputs. We don't test summing just to ensure that 1 + 1 = 2, but we will test the integer, decimal and also negative inputs separately and verify the results. In some cases, we might also be interested in the maximum value of data types and so on.

The last test verifies whether the divide() method really causes an exception at the zero divider. As you can see, we don't have to bother with try-catch blocks, just add the @expectedException annotation and put the exceptions class we expect here. If the exception does not occur, the test fails. More methods should be added to test multiple cases of throwing an exception in this manner.

Available assert methods

In addition to the assertEquals() method, we can use many more, certainly try to use the most convenient method, it makes easy to read test failure reports and, of course, the subsequent fix. The list of assert methods is quite exhausting and you can easily see it in the IDE, so let us just mention the most important ones:

  • assertContain­s($needle, $hay) - Checks, whether $hay (array) contains a given value ($needle).
  • assertCount($ex­pectedQuantity, $collection) - Checks, whether $collection contains $expectedQuantity of items.
  • assertFalse($va­lue) - Checks, whether value is false.
  • assertTrue($va­lue) - Checks, whether value is true.
  • assertNotEqual­s($expectedVa­lue, $value) - Checks whether the values are NOT the same. A similar "Not" method has most of the assertions, so we no longer mention them here.
  • assertGreater­Than($expected­Value, $value) - Checks, whether $value is greater than $expectedValue.
  • assertGreater­ThanOrEqual($ex­pectedValue, $value) - Checks, whether $value is greater than or equals to $expectedValue.
  • assertLessThan($ex­pectedValue, $value) - Checks, whether $value is less than $expectedValue.
  • assertLessTha­nOrEqual($expec­tedValue, $value) - Checks, whether $value is less than or equals to $expectedValue.
  • assertNull($va­lue) - Checks, whether value is null.
  • assertSame($ex­pectedValue, $value) - It works just like assertEquals(), but checks the match of the data types as well.

There are also assertions for testing attributes, strings (e.g. if the string starts with...), fields, folders, files, and XML.

assertThat()

Very interesting is the assertThat() method, which allows for alternative approach to assertions. For example in Java (PHPUnit is quite clearly based on JUnit), this method also provides options for additional data type control, we will mention it in PHP rather for interest and let's check out, how the first assert of our tests would look like by using assertThat(). To recall the original version:

$this->assertEquals(2, $this->calculator->sum(1, 1));

And now with assertThat():

$this->assertThat(
    $this->calculator->sum(1, 1),
    $this->equalTo(
        2
    )
);

The advantage is that the code looks more like an English sentence. The disadvantage is higher code volume and recursive calls. For more complex assertions, this method could be advantageous.

Running the tests

We run the tests by command:

test run unit

We'll see results that look like this:

> C:\xampp\php\php.exe codecept.phar run unit
Codeception PHP Testing Framework v2.3.8
Powered by PHPUnit 6.5.6 by Sebastian Bergmann and contributors.

Unit Tests (5) -------------------
+ CalculatorTest: Summation (0.00s)
+ CalculatorTest: Subtraction (0.00s)
+ CalculatorTest: Multiplication (0.00s)
+ CalculatorTest: Division (0.01s)
+ CalculatorTest: Division exception (0.00s)
----------------------------------


Time: 314 ms, Memory: 8.00MB

OK (5 tests, 13 assertions)

Process finished with exit code 0 at 19:32:44.
Execution time: 467 ms.

If Codeception reports a problem with the unavailability of the "php" command, open the codeception.yml configuration file and add row lint: false into the settings section.

...
settings:
    bootstrap: _bootstrap.php
    colors: false
    memory_limit: 1024M
    lint: false
...

In some versions, the output state may be incorrectly evaluated.

Let's try to make a mistake in the calculator, for example by commenting the invocation of the dividing by zero exception and always return a value of 1:

public function divide($a, $b)
{
    //if ($b == 0)
    //  throw new \InvalidArgumentException("Can not divide by zero!");
    return 1;
}

And let's run our tests again:

> C:\xampp\php\php.exe codecept.phar run unit
Codeception PHP Testing Framework v2.3.8
Powered by PHPUnit 6.5.6-2 by Sebastian Bergmann and contributors.

Unit Tests (5) -------------------
+ CalculatorTest: Summation (0.00s)
+ CalculatorTest: Subtraction (0.00s)
+ CalculatorTest: Multiplication (0.00s)
x CalculatorTest: Division (0.01s)
x CalculatorTest: Division exception (0.00s)
----------------------------------


Time: 334 ms, Memory: 8.00MB

There were 2 failures:

---------
1) CalculatorTest: Division
 Test  tests\unit\CalculatorTest.php:testDivision
Failed asserting that 1 matches expected 2.
#1  C:\Users\David\PhpstormProjects\calculator\tests\unit\CalculatorTest.php:44
#2  CalculatorTest->testDivision

---------
2) CalculatorTest: Division exception
 Test  tests\unit\CalculatorTest.php:testDivisionException
Failed asserting that exception of type "InvalidArgumentException" is thrown.

FAILURES!
Tests: 5, Assertions: 11, Failures: 2.

Process finished with exit code 1 at 19:35:07.
Execution time: 490 ms.

We can see that the bug is captured and we're alerted to it. Both the division test and the division exception test have not passed. We can reset the code to its original state.

In the next lesson, PHPUnit DataProvider and BestPractices, we will show the selected source codes of interesting unit tests of commercial systems, to get an overview of how to test more complex situations.


 

Did you have a problem with anything? Download the sample application below and compare it with your project, you will find the error easily.

Download

By downloading the following file, you agree to the license terms

Downloaded 4x (1.06 MB)
Application includes source codes in language PHP

 

Previous article
Introduction to unit tests in PHP and PHPUnit installation
All articles in this section
Testing in PHP
Skip article
(not recommended)
PHPUnit DataProvider and BestPractices
Article has been written for you by David Capka Hartinger
Avatar
User rating:
No one has rated this quite yet, be the first one!
The author is a programmer, who likes web technologies and being the lead/chief article writer at ICT.social. He shares his knowledge with the community and is always looking to improve. He believes that anyone can do what they set their mind to.
Unicorn university David learned IT at the Unicorn University - a prestigious college providing education on IT and economics.
Activities