Requires: Composer, PHP>7.3, Xdebug
Installation
Use composer to install PHPUnit:
$ composer require phpunit/phpunit
Next, update composer.json so the autoloader can find your tests:
{
"autoload": {
"psr-4": {
"Tests\\": "tests/"
}
}
}
Then run this command to create the PHPUnit configuration file:
vendor/bin/phpunit --generate-configuration
Write a test
We are going to test a very simple repository:
<?php
namespace App\Repositories;
use App\Entities\Person;
use App\Exceptions\EntityNotFoundException;
class PersonRepository
{
protected $db;
public function __construct(\PDO $db)
{
$this->db = $db;
}
public function find(int $id): Person
{
$stmt = $this->db->prepare(
'SELECT id, name, email FROM persons WHERE id = :id'
);
if (! $stmt) {
throw new \RuntimeException(
"unable to prepare statement"
);
}
if (! $stmt->execute(compact('id'))) {
throw new \RuntimeException(
"unable to execute statement: {$stmt->queryString}"
);
}
$person = $stmt->fetchObject(Person::class);
if (! $person) {
throw new EntityNotFoundException(
Person::class,
compact('id')
);
}
return $person;
}
}
The test class (note the Test
suffix on the classname):
<?php
namespace Tests\App\Repositories;
use App\Entities\Person;
use App\Exceptions\EntityNotFoundException;
use App\Repositories\PersonRepository;
use PHPUnit\Framework\TestCase;
/**
* @covers \App\Repositories\PersonRepository
*/
class PersonRepositoryTest extends TestCase
{
protected $pdo;
public function setUp(): void
{
//
// this setUp method is intended to crate
// the environment in which the test will
// take place.
// Typically, the database state...
//
// I would strongly recommend to use SQLite
// in-memory so you don't pollute your
// actual database with test data.
//
$this->pdo = new \PDO("sqlite::memory:");
$this->pdo->exec("
CREATE TABLE `persons` (
id INT PRIMARY KEY,
name STRING,
email STRING
)
");
$stmt = $this->pdo->prepare(
"INSERT INTO `persons` VALUES (:id, :name, :email)"
);
$stmt->execute([
'id' => 1,
'name' => 'Nathalie PORTMAN',
'email' => 'nathalie.portman@example.com'
]);
$stmt->execute([
'id' => 2,
'name' => 'Jack BLACK',
'email' => 'jack.black@example.com'
]);
$stmt->execute([
'id' => 3,
'name' => 'Leonardo DICAPRIO',
'email' => 'leonardo.dicaprio@oexample.com'
]);
}
/**
* @covers \App\Repositories\PersonRepository::find
* @dataProvider findProvider
*/
public function testFind(int $id, array $data)
{
//
// In this test we will test several valid
// use-cases for the find() method. They are
// described by the scenarios returned by
// findPovider() (see below).
//
// Each scenario is run after another, passing
// the variables to the testFind() function,
// allowing us to test several conditions with
// the same test.
//
$repository = new PersonRepository($this->pdo);
$person = $repository->find(1);
$this->assertInstanceOf(
Person::class,
$person,
"The return of 'App\Repositories\PersonRepository::find' should be an 'App\Entities\Person' instance"
);
$this->assertEquals(
'Nathalie PORTMAN',
$person->name,
"The name of the person with id '{$id}' should be '{$data['name']}'"
);
$this->assertEquals(
'nathalie.portman@example.com',
$person->email,
"The email of the person with id '{$id}' should be '{$data['email']}'"
);
}
/**
* @covers \App\Repositories\PersonRepository::find
*/
public function testFindFailsWhenDatabaseIsNotReady()
{
//
// in this test (and the following) we will
// test invalid use-cases. Places where the
// execution of the find() method is expected
// to fail - in our case by throwing an
// exception.
//
$this->expectException(\RuntimeException::class);
$this->expectExceptionMessage("unable to prepare statement");
// for this test we connect to an empty database
// so we are sure the query will fail.
$repository = new PersonRepository(new \PDO("sqlite::memory:"));
$repository->find(1);
}
/**
* @covers \App\Repositories\PersonRepository::find
*/
public function testFindFailsWhenQueryFails()
{
$this->expectException(\RuntimeException::class);
$this->expectExceptionMessage("unable to execute statement");
// let's create a PDO mock that returns statements
// whose execute method will always return false
$pdo = new class("sqlite::memory:") extends \PDO {
public function prepare($statement, $options = null) {
$stmt = new class {
public function execute() {
return false;
}
};
$stmt->queryString = $statement;
return $stmt;
}
};
$repository = new PersonRepository($pdo);
$repository->find(1);
}
/**
* @covers \App\Repositories\PersonRepository::find
*/
public function testFindFailsWhenNoResultFound()
{
$this->expectException(EntityNotFoundException::class);
$this->expectExceptionMessage("unable to find entity");
$repository = new PersonRepository($this->pdo);
$repository->find(4);
}
public function findProvider(): array
{
//
// This method is not a test but rather
// a data-provider (hence the name) whose
// job is to describe scenarios.
//
// Those scenarios are identified by their
// name (which is very helpful when a test
// fails) and consist of values that will
// be passed as arguments of a test method
// (see testFind() above.)
//
return [
"Scenario 1 : user with id '1' is 'Nathalie PORTMAN'" => [
'id' => 1,
'data' => [
'name' => "Nathalie PORTMAN",
'email' => "nathalie.portman@example.com",
],
],
"Scenario 2 : user with id '2' is 'Jack BLACK'" => [
'id' => 2,
'data' => [
'name' => "Jack BLACK",
'email' => "jack.black@example.com",
],
],
"Scenario 3 : user with id '3' is 'Leonardo DICAPRIO'" => [
'id' => 3,
'data' => [
'name' => "Leonardo DICAPRIO",
'email' => "leonardo.dicaprio@oexample.com",
],
],
];
}
}
Execute your test
Now you just run:
$ vendor/bin/phpunit
And you should get the following result:
PHPUnit 9.5.0 by Sebastian Bergmann and contributors.
Runtime: PHP 7.4.3
Configuration: /home/benjamin/Workspace/bdelespierre/phpunit-demo/phpunit.xml
...... 6 / 6 (100%)
Time: 00:00.005, Memory: 6.00 MB
OK (6 tests, 15 assertions)
Get the coverage report
Run:
$vendor/bin/phpunit --coverage-text
And you should get the following result:
PHPUnit 9.5.0 by Sebastian Bergmann and contributors.
Runtime: PHP 7.4.3 with Xdebug 2.9.2
Configuration: /home/benjamin/Workspace/bdelespierre/phpunit-demo/phpunit.xml
...... 6 / 6 (100%)
Time: 00:00.058, Memory: 10.00 MB
OK (6 tests, 15 assertions)
Code Coverage Report:
2020-12-09 15:21:57
Summary:
Classes: 100.00% (1/1)
Methods: 100.00% (2/2)
Lines: 100.00% (16/16)
App\Entities\Person
Methods: ( 0/ 0) Lines: ( 0/ 0)
App\Exceptions\EntityNotFoundException
Methods: ( 0/ 0) Lines: ( 0/ 0)
App\Repositories\PersonRepository
Methods: 100.00% ( 2/ 2) Lines: 100.00% ( 16/ 16)
Run tests every time you commit
Want to make sure you don't break things before comitting? Easy!
First you need to create a file in .git/hooks/pre-commit
:
#!/bin/bash
vendor/bin/phpunit
Then make it executable:
chmod +x .git/hooks/pre-commit
And voilà!
Don't forget to leave a like and tell me in comments what you think of this article.
From the same author:
Top comments (2)
Hi Benjamin! Thanks for this great useful post!
I think I detected a small mistake
composer require phpunit/phpunit
instead ofcomposer install phpunit/phpunit
Done 👍