The primary means of testing PHP code is to use PHPUnit, which is based on a methodology called Unit Testing. The philosophy behind unit testing is quite simple: you break down your code into the smallest possible logical units. You then test each unit in isolation to confirm that it performs as expected. These expectations are codified into a series of assertions. If all assertions return TRUE, then the unit has passed the test.
<hash>:php -r "copy('https://getcomposer.org/installer', 'composer-setup.php');"
php -r "if (hash_file('SHA384', 'composer-setup.php') === '<hash>') {
echo 'Installer verified';
} else {
echo 'Installer corrupt'; unlink('composer-setup.php');
} echo PHP_EOL;"
php composer-setup.php
php -r "unlink('composer-setup.php');"composer.json file that contains a series of directives outlining project parameters and dependencies. A full description of these directives is beyond the scope of this book; however, for the purposes of this recipe, we create a minimal set of directives using the key parameter require. You will also note that the contents of the file are in JavaScript Object Notation (JSON) format:{
"require-dev": {
"phpunit/phpunit": "*"
}
}
php composer.phar install

vendor folder that Composer will create if it does not already exist. The primary command to invoke PHPUnit is then symbolically linked into the vendor/bin folder. If you place this folder in your PATH, all you need do is to run this command, which checks the version and incidentally confirms the installation:
phpunit --version
chap_13_unit_test_simple.php file that contains the add() function:<?php
function add($a = NULL, $b = NULL)
{
return $a + $b;
}PHPUnit\Framework\TestCase. If you are testing a library of functions, at the beginning of the test class, include the file that contains function definitions. You would then write methods that start with the word test, usually followed by the name of the function you are testing, and possibly some additional CamelCase words to further describe the test. For the purposes of this recipe, we will define a SimpleTest test class:<?php
use PHPUnit\Framework\TestCase;
require_once __DIR__ . '/chap_13_unit_test_simple.php';
class SimpleTest extends TestCase
{
// testXXX() methods go here
}See also section gives you the documentation reference for the complete list of assertions. An assertion is a PHPUnit method that compares a known value against a value produced by that which you wish to test. An example is assertEquals(), which checks to see whether the first argument equals the second. The following example tests a method called add() and confirms 2 is the return value for add(1,1):public function testAdd()
{
$this->assertEquals(2, add(1,1));
}$this->assertNotEquals(3, add(1,1));
assertRegExp(). Assume, for this illustration, that we are testing a function that produces an HTML table out of a multidimensional array:function table(array $a)
{
$table = '<table>';
foreach ($a as $row) {
$table .= '<tr><td>';
$table .= implode('</td><td>', $row);
$table .= '</td></tr>';
}
$table .= '</table>';
return $table;
}<table>, one or more characters, followed by </table>. Further, we wish to confirm that a <td>B</td> element exists. When writing the test, we build a test array that consists of three sub-arrays containing the letters A—C, D—F, and G—I. We then pass the test array to the function, and run assertions against the result:public function testTable()
{
$a = [range('A', 'C'),range('D', 'F'),range('G','I')];
$table = table($a);
$this->assertRegExp('!^<table>.+</table>$!', $table);
$this->assertRegExp('!<td>B</td>!', $table);
}Demo class:<?php
class Demo
{
public function add($a, $b)
{
return $a + $b;
}
public function sub($a, $b)
{
return $a - $b;
}
// etc.
}SimpleClassTest test class, instead of including the library file, we include the file that represents the Demo class. We need an instance of Demo in order to run tests. For this purpose, we use a specially designed setup() method, which is run before each test. Also, you will note a teardown() method, which is run immediately after each test:<?php
use PHPUnit\Framework\TestCase;
require_once __DIR__ . '/Demo.php';
class SimpleClassTest extends TestCase
{
protected $demo;
public function setup()
{
$this->demo = new Demo();
}
public function teardown()
{
unset($this->demo);
}
public function testAdd()
{
$this->assertEquals(2, $this->demo->add(1,1));
}
public function testSub()
{
$this->assertEquals(0, $this->demo->sub(1,1));
}
// etc.
}setup() and teardown() could also be used to add or remove test data.VisitorOps. The new class will include methods to add, remove, and find visitors. Note that we've also added a method to return the latest SQL statement executed:<?php
require __DIR__ . '/../Application/Database/Connection.php';
use Application\Database\Connection;
class VisitorOps
{
const TABLE_NAME = 'visitors';
protected $connection;
protected $sql;
public function __construct(array $config)
{
$this->connection = new Connection($config);
}
public function getSql()
{
return $this->sql;
}
public function findAll()
{
$sql = 'SELECT * FROM ' . self::TABLE_NAME;
$stmt = $this->runSql($sql);
while ($row = $stmt->fetch(PDO::FETCH_ASSOC)) {
yield $row;
}
}
public function findById($id)
{
$sql = 'SELECT * FROM ' . self::TABLE_NAME;
$sql .= ' WHERE id = ?';
$stmt = $this->runSql($sql, [$id]);
return $stmt->fetch(PDO::FETCH_ASSOC);
}
public function removeById($id)
{
$sql = 'DELETE FROM ' . self::TABLE_NAME;
$sql .= ' WHERE id = ?';
return $this->runSql($sql, [$id]);
}
public function addVisitor($data)
{
$sql = 'INSERT INTO ' . self::TABLE_NAME;
$sql .= ' (' . implode(',',array_keys($data)) . ') ';
$sql .= ' VALUES ';
$sql .= ' ( :' . implode(',:',array_keys($data)) . ') ';
$this->runSql($sql, $data);
return $this->connection->pdo->lastInsertId();
}
public function runSql($sql, $params = NULL)
{
$this->sql = $sql;
try {
$stmt = $this->connection->pdo->prepare($sql);
$result = $stmt->execute($params);
} catch (Throwable $e) {
error_log(__METHOD__ . ':' . $e->getMessage());
return FALSE;
}
return $stmt;
}
}setup() method.setup() method.teardown() method.VisitorService, which makes use of the VisitorOps class discussed earlier:<?php
require_once __DIR__ . '/VisitorOps.php';
require_once __DIR__ . '/../Application/Database/Connection.php';
use Application\Database\Connection;
class VisitorService
{
protected $visitorOps;
public function __construct(array $config)
{
$this->visitorOps = new VisitorOps($config);
}
public function showAllVisitors()
{
$table = '<table>';
foreach ($this->visitorOps->findAll() as $row) {
$table .= '<tr><td>';
$table .= implode('</td><td>', $row);
$table .= '</td></tr>';
}
$table .= '</table>';
return $table;
}$visitorOps property. This allows us to insert a mock class in place of the real VisitorOps class:public function getVisitorOps()
{
return $this->visitorOps;
}
public function setVisitorOps(VisitorOps $visitorOps)
{
$this->visitorOps = $visitorOps;
}
} // closing brace for VisitorServiceVisitorOpsMock mock class that mimics the functionality of its parent class. Class constants and properties are inherited. We then add mock test data, and a getter in case we need access to the test data later:<?php
require_once __DIR__ . '/VisitorOps.php';
class VisitorOpsMock extends VisitorOps
{
protected $testData;
public function __construct()
{
$data = array();
for ($x = 1; $x <= 3; $x++) {
$data[$x]['id'] = $x;
$data[$x]['email'] = $x . 'test@unlikelysource.com';
$data[$x]['visit_date'] =
'2000-0' . $x . '-0' . $x . ' 00:00:00';
$data[$x]['comments'] = 'TEST ' . $x;
$data[$x]['name'] = 'TEST ' . $x;
}
$this->testData = $data;
}
public function getTestData()
{
return $this->testData;
}findAll() to return test data using yield, just as in the parent class. Note that we still build the SQL string, as this is what the parent class does:public function findAll()
{
$sql = 'SELECT * FROM ' . self::TABLE_NAME;
foreach ($this->testData as $row) {
yield $row;
}
}findById() we simply return that array key from $this->testData. For removeById(), we unset the array key supplied as a parameter from $this->testData:public function findById($id)
{
$sql = 'SELECT * FROM ' . self::TABLE_NAME;
$sql .= ' WHERE id = ?';
return $this->testData[$id] ?? FALSE;
}
public function removeById($id)
{
$sql = 'DELETE FROM ' . self::TABLE_NAME;
$sql .= ' WHERE id = ?';
if (empty($this->testData[$id])) {
return 0;
} else {
unset($this->testData[$id]);
return 1;
}
}id parameter might not be supplied, as the database would normally auto-generate this for us. To get around this, we check for the id parameter. If not set, we find the largest array key and increment:public function addVisitor($data)
{
$sql = 'INSERT INTO ' . self::TABLE_NAME;
$sql .= ' (' . implode(',',array_keys($data)) . ') ';
$sql .= ' VALUES ';
$sql .= ' ( :' . implode(',:',array_keys($data)) . ') ';
if (!empty($data['id'])) {
$id = $data['id'];
} else {
$keys = array_keys($this->testData);
sort($keys);
$id = end($keys) + 1;
$data['id'] = $id;
}
$this->testData[$id] = $data;
return 1;
}
} // ending brace for the class VisitorOpsMockVisitorServiceTest.php presented previously, calling it VisitorServiceTestAnonClass.php:<?php
use PHPUnit\Framework\TestCase;
require_once __DIR__ . '/VisitorService.php';
require_once __DIR__ . '/VisitorOps.php';
class VisitorServiceTestAnonClass extends TestCase
{
protected $visitorService;
protected $dbConfig = [
'driver' => 'mysql',
'host' => 'localhost',
'dbname' => 'php7cookbook_test',
'user' => 'cook',
'password' => 'book',
'errmode' => PDO::ERRMODE_EXCEPTION,
];
protected $testData;setup(), we define an anonymous class that extends VisitorOps. We only need to override the findAll() method:public function setup()
{
$data = array();
for ($x = 1; $x <= 3; $x++) {
$data[$x]['id'] = $x;
$data[$x]['email'] = $x . 'test@unlikelysource.com';
$data[$x]['visit_date'] =
'2000-0' . $x . '-0' . $x . ' 00:00:00';
$data[$x]['comments'] = 'TEST ' . $x;
$data[$x]['name'] = 'TEST ' . $x;
}
$this->testData = $data;
$this->visitorService =
new VisitorService($this->dbConfig);
$opsMock =
new class ($this->testData) extends VisitorOps {
protected $testData;
public function __construct($testData)
{
$this->testData = $testData;
}
public function findAll()
{
return $this->testData;
}
};
$this->visitorService->setVisitorOps($opsMock);
}testShowAllVisitors(), when $this->visitorService->showAllVisitors() is executed, the anonymous class is called by the visitor service, which in turn calls the overridden findAll():public function teardown()
{
unset($this->visitorService);
}
public function testShowAllVisitors()
{
$result = $this->visitorService->showAllVisitors();
$this->assertRegExp('!^<table>.+</table>$!', $result);
foreach ($this->testData as $key => $value) {
$dataWeWant = '!<td>' . $key . '</td>!';
$this->assertRegExp($dataWeWant, $result);
}
}
}getMockBuilder(). Although this approach does not allow a great deal of finite control over the mock object produced, it's extremely useful in situations where you only need to confirm that an object of a certain class is returned, and when a specified method is run, this method returns some expected value.VisitorServiceTestAnonClass; the only difference is in how an instance of VisitorOps is supplied in setup(), in this case, using getMockBuilder(). Note that although we did not use with() in this example, it is used to feed controlled parameters to the mocked method:<?php
use PHPUnit\Framework\TestCase;
require_once __DIR__ . '/VisitorService.php';
require_once __DIR__ . '/VisitorOps.php';
class VisitorServiceTestAnonMockBuilder extends TestCase
{
// code is identical to VisitorServiceTestAnon
public function setup()
{
$data = array();
for ($x = 1; $x <= 3; $x++) {
$data[$x]['id'] = $x;
$data[$x]['email'] = $x . 'test@unlikelysource.com';
$data[$x]['visit_date'] =
'2000-0' . $x . '-0' . $x . ' 00:00:00';
$data[$x]['comments'] = 'TEST ' . $x;
$data[$x]['name'] = 'TEST ' . $x;
}
$this->testData = $data;
$this->visitorService =
new VisitorService($this->dbConfig);
$opsMock = $this->getMockBuilder(VisitorOps::class)
->setMethods(['findAll'])
->disableOriginalConstructor()
->getMock();
$opsMock->expects($this->once())
->method('findAll')
->with()
->will($this->returnValue($this->testData));
$this->visitorService->setVisitorOps($opsMock);
}
// remaining code is the same
}First, you need to install PHPUnit, as discussed in steps 1 to 5. Be sure to include vendor/bin in your PATH so that you can run PHPUnit from the command line.
Next, define a chap_13_unit_test_simple.php program file with a series of simple functions, such as add(), sub() and so on, as discussed in step 1. You can then define a simple test class contained in SimpleTest.php as mentioned in steps 2 and 3.
Assuming phpunit is in your PATH, from a terminal window, change to the directory containing the code developed for this recipe, and run the following command:
phpunit SimpleTest SimpleTest.php
You should see the following output:

Make a change in SimpleTest.php so that the test will fail (step 4):
public function testDiv()
{
$this->assertEquals(2, div(4, 2));
$this->assertEquals(99, div(4, 0));
}Here is the revised output:

Next, add the table() function to chap_13_unit_test_simple.php (step 5), and testTable() to SimpleTest.php (step 6). Re-run the unit test and observe the results.
To test a class, copy the functions developed in chap_13_unit_test_simple.php to a Demo class (step 7). After making the modifications to SimpleTest.php suggested in step 8, re-run the simple test and observe the results.
First, create an example class to be tested, VisitorOps, shown in step 2 in this subsection. You can now define a class we will call SimpleDatabaseTest to test VisitorOps. First of all, use require_once to load the class to test. (We will discuss how to incorporate autoloading in the next recipe!) Then define key properties, including test database configuration and test data. You could use php7cookbook_test as the test database:
<?php
use PHPUnit\Framework\TestCase;
require_once __DIR__ . '/VisitorOps.php';
class SimpleDatabaseTest extends TestCase
{
protected $visitorOps;
protected $dbConfig = [
'driver' => 'mysql',
'host' => 'localhost',
'dbname' => 'php7cookbook_test',
'user' => 'cook',
'password' => 'book',
'errmode' => PDO::ERRMODE_EXCEPTION,
];
protected $testData = [
'id' => 1,
'email' => 'test@unlikelysource.com',
'visit_date' => '2000-01-01 00:00:00',
'comments' => 'TEST',
'name' => 'TEST'
];
}Next, define setup(), which inserts the test data, and confirms that the last SQL statement was INSERT. You should also check to see whether the return value was positive:
public function setup()
{
$this->visitorOps = new VisitorOps($this->dbConfig);
$this->visitorOps->addVisitor($this->testData);
$this->assertRegExp('/INSERT/', $this->visitorOps->getSql());
}After that, define teardown(), which removes the test data and confirms that the query for id = 1 comes back as FALSE:
public function teardown()
{
$result = $this->visitorOps->removeById(1);
$result = $this->visitorOps->findById(1);
$this->assertEquals(FALSE, $result);
unset($this->visitorOps);
}The first test is for findAll(). First, confirm the data type of the result. You could take the topmost element using current(). We confirm there are five elements, that one of them is name, and that the value is the same as that in the test data:
public function testFindAll()
{
$result = $this->visitorOps->findAll();
$this->assertInstanceOf(Generator::class, $result);
$top = $result->current();
$this->assertCount(5, $top);
$this->assertArrayHasKey('name', $top);
$this->assertEquals($this->testData['name'], $top['name']);
}The next test is for findById(). It is almost identical to testFindAll():
public function testFindById()
{
$result = $this->visitorOps->findById(1);
$this->assertCount(5, $result);
$this->assertArrayHasKey('name', $result);
$this->assertEquals($this->testData['name'], $result['name']);
}You do not need to bother with a test for removeById() as this is already done in teardown(). Likewise, there is no need to test runSql() as this is done as part of the other tests.
First, define a VisitorService service class as described in steps 2 and 3 in this subsection. Next, define a VisitorOpsMock mock class, which is discussed in steps 4 to 7.
You are now in a position to develop a test, VisitorServiceTest, for the service class. Note that you need provide your own database configuration as it is a best practice to use a test database instead of the production version:
<?php
use PHPUnit\Framework\TestCase;
require_once __DIR__ . '/VisitorService.php';
require_once __DIR__ . '/VisitorOpsMock.php';
class VisitorServiceTest extends TestCase
{
protected $visitorService;
protected $dbConfig = [
'driver' => 'mysql',
'host' => 'localhost',
'dbname' => 'php7cookbook_test',
'user' => 'cook',
'password' => 'book',
'errmode' => PDO::ERRMODE_EXCEPTION,
];
}In setup(), create an instance of the service, and insert VisitorOpsMock in place of the original class:
public function setup()
{
$this->visitorService = new VisitorService($this->dbConfig);
$this->visitorService->setVisitorOps(new VisitorOpsMock());
}
public function teardown()
{
unset($this->visitorService);
}In our test, which produces an HTML table from the list of visitors, you can then look for certain elements, knowing what to expect in advance as you have control over the test data:
public function testShowAllVisitors()
{
$result = $this->visitorService->showAllVisitors();
$this->assertRegExp('!^<table>.+</table>$!', $result);
$testData = $this->visitorService->getVisitorOps()->getTestData();
foreach ($testData as $key => $value) {
$dataWeWant = '!<td>' . $key . '</td>!';
$this->assertRegExp($dataWeWant, $result);
}
}
}You might then wish to experiment with the variations suggested in the last two subsections, Using Anonymous Classes as Mock Objects, and Using Mock Builder.
Other assertions test operations on numbers, strings, arrays, objects, files, JSON, and XML, as summarized in the following table:
|
Category |
Assertions |
|---|---|
|
General |
|
|
Numeric |
|
|
String |
|
|
Array/iterator |
|
|
File |
|
|
Objects |
|
|
JSON |
|
|
XML |
|
composer.json file directives, see https://getcomposer.org/doc/04-schema.md.getMockBuilder() in detail here: https://phpunit.de/manual/current/en/phpunit-book.html#test-doubles.mock-objects