DEV Community 👩‍💻👨‍💻

DEV Community 👩‍💻👨‍💻 is a community of 968,873 amazing developers

We're a place where coders share, stay up-to-date and grow their careers.

Create account Log in
Benjamin Delespierre
Benjamin Delespierre

Posted on

PHPUnit from scratch in 5 minutes

Requires: Composer, PHP>7.3, Xdebug

Installation

Use composer to install PHPUnit:

composer install 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

Simply 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 (0)

Thank you.

Thanks for visiting DEV, we’ve worked really hard to cultivate this great community and would love to have you join us. If you’d like to create an account, you can sign up here.