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:
- assertContains($needle, $hay) - Checks, whether $hay (array) contains a given value ($needle).
- assertCount($expectedQuantity, $collection) - Checks, whether $collection contains $expectedQuantity of items.
- assertFalse($value) - Checks, whether value is
false
. - assertTrue($value) - Checks, whether value is
true
. - assertNotEquals($expectedValue, $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.
- assertGreaterThan($expectedValue, $value) - Checks, whether $value is greater than $expectedValue.
- assertGreaterThanOrEqual($expectedValue, $value) - Checks, whether $value is greater than or equals to $expectedValue.
- assertLessThan($expectedValue, $value) - Checks, whether $value is less than $expectedValue.
- assertLessThanOrEqual($expectedValue, $value) - Checks, whether $value is less than or equals to $expectedValue.
- assertNull($value) - Checks, whether value is
null
. - assertSame($expectedValue, $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