DEV Community

Benjamin Delespierre
Benjamin Delespierre

Posted on • Updated on

PHPUnit from scratch in 5 minutes

Requires: Composer, PHP>7.3, Xdebug

Installation

Use composer to install PHPUnit:

$ composer require phpunit/phpunit
Enter fullscreen mode Exit fullscreen mode

Next, update composer.json so the autoloader can find your tests:

{
    "autoload": {
        "psr-4": {
            "Tests\\": "tests/"
        }
    }
}
Enter fullscreen mode Exit fullscreen mode

Then run this command to create the PHPUnit configuration file:

vendor/bin/phpunit --generate-configuration
Enter fullscreen mode Exit fullscreen mode

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;
    }
}
Enter fullscreen mode Exit fullscreen mode

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",
                ],
            ],
        ];
    }
}
Enter fullscreen mode Exit fullscreen mode

Execute your test

Now you just run:

$ vendor/bin/phpunit
Enter fullscreen mode Exit fullscreen mode

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)
Enter fullscreen mode Exit fullscreen mode

Get the coverage report

Run:

$vendor/bin/phpunit --coverage-text
Enter fullscreen mode Exit fullscreen mode

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)
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

Then make it executable:

chmod +x .git/hooks/pre-commit
Enter fullscreen mode Exit fullscreen mode

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)

Collapse
 
renzocastillo profile image
Renzo Castillo

Hi Benjamin! Thanks for this great useful post!
I think I detected a small mistake

composer require phpunit/phpunit instead of composer install phpunit/phpunit

Collapse
 
bdelespierre profile image
Benjamin Delespierre

Done 👍