Even good programmers make mistakes. The difference between a good programmer and a bad one is that a good one detects it sooner by using automated tests.

  • “One who doesn't test is doomed to repeat his or her own mistakes.” (wise proverb)
  • “When we get rid of one error, another one appears.” (Murphy's Law)
  • “Do test, do test, do test” (Martin Iljič Fowler)

Have you ever written the following code in PHP?

$obj = new MyClass;
$result = $obj->process($input);


So, have you ever dumped a function call result just to check by eye that it returns what it should return? You surely do it many times per day. If everything works, do you delete this code and expect that the class will not be broken in the future? Murphy's Law guarantees the opposite :-)

In fact, we wrote the test. And if we didn't delete it we could run it any time in the future to verify that everything still works as it should. You may create a large amount of these tests over time, so it would be nice if we were able to run them automatically. It would be useful to slightly modify test not to require our inspection, simply to be able to check itself.

And Nette Tester helps exactly with that.

Installation and requirements

Minimal required PHP version by Tester is 5.6.0.

Preferred way of installation is by Composer and every following example assumes that. But the Tester can be used without it. It will be also shown below.

Installation by Composer

Let's assume you have the Composer up and running and an application named demo with the following structure:

├── src/           # application code we want to test
├── tests/         # tests we are writing
├── vendor/
└── composer.json

Navigate in CLI terminal to the application directory and add Tester as a dependency using Composer:

cd demo
composer require --dev nette/tester

Manual installation

Download Tester from GitHub and extract it or clone its repository by git clone https://github.com/nette/tester.git. Directory structure of our demo application is now as follows:

├── src/           # application code we want to test
├── tester/        # source of downloaded Tester
│   ├── src/
│   ├── tests/
│   ├── ...
│   └── readme.md
└── tests/         # tests we are writing

Running the Tester

Nette Tester is run from the command line. We can try it, without any arguments it will only show a help summary.

cd demo
php vendor/nette/tester/src/tester.php  # installation by Composer
php tester/src/tester.php               # manual installation

If Tester was installed by Composer we can also use the more convenient shortcut script:

cd demo
vendor/bin/tester      # UNIX
vendor\bin\tester.bat  # Windows

For simplification we only use the tester command further in this document. But one of the ways mentioned above with a full path is required to run it.

Running the tests

Our application has no tests yet. We create a simple class to be tested and we save it to file src/Greeting.php


class Greeting
    public function say($name)
        if (!$name) {
            throw new InvalidArgumentException('Invalid name');
        return "Hello $name";

Let's write a test now. We save it into file tests/greeting.phpt. Don't be discouraged by its length, it is shown later how to simplify it.


use Tester\Assert;

# Load Tester library
require __DIR__ . '/../vendor/autoload.php';       # installation by Composer
require __DIR__ . '/../tester/src/bootstrap.php';  # manual installation

# Load the tested class. Composer or your autoloader surely takes
# care of that in practice.
require __DIR__ . '/../src/Greeting.php';

# Adjust PHP behaviour and enable some Tester features (described later)

$o = new Greeting;

Assert::same('Hello John', $o->say('John'));  # we expect the same

Assert::exception(function () use ($o) {       # we expect an exception
}, InvalidArgumentException::class, 'Invalid name');

The test is written, we can run it from command line for the first time:

cd tests
php greeting.phpt

We have run the test as an ordinary PHP script for the first time. Even it seems common, big potential is hidden in it. We can step through the test in IDE or load it via web browser.

The first run discloses syntactic errors and if we didn't make a typo, the test ends without error report. Let's change an assertion in the test to Assert::same('Hi John', $o->say('John')); and let's watch what happens when run.

As our application grows, number of tests grows with it. It would not be practical to run tests one by one. We use the Tester for running:

cd demo
tester tests/greeting.phpt  # we run a single test
tester tests                # we run all tests in directory

To see how more tests running looks like, let's try to run all tests which test the Tester itself. They are part of its installation:

tester vendor/nette/tester/tests  # installation by Composer
tester tester/tests               # manual installation

How the Tester runs

The Tester walks through the passed directories. It creates a list of found tests and it runs them one by one. It runs every test as a new process, every test runs completely isolated from others.

The Tester searches for *.phpt and *Test.php files. The previously failing tests run as first.

The Tester runs PHP processes with -n option, so without php.ini. More details in the Own php.ini chapter.

Test results evaluation

The Tester prints test results continuously during testing:

  • . (dot) – test passed
  • s – test has been skipped
  • F – test failed

Output may look like:

 _____ ___  ___ _____ ___  ___
|_   _/ __)( __/_   _/ __)| _ )
  |_| \___ /___) |_| \___ |_|_\  v2.0.0

PHP 7.1.3 (cli) | php -n | 8 threads


-- FAILED: tests/greeting.phpt
   Failed: 'Hello John' should be
       ... 'Hi John'

   in src/Framework/Assert.php(370)
   in src/Framework/Assert.php(52) Tester\Assert::fail()
   in tests/greeting.phpt(6) Tester\Assert::same()

FAILURES! (35 tests, 1 failures, 1 skipped, 1.7 seconds)

35 tests were run, one failed, one was skipped. Tester's exit code is zero if no one fails. Non-zero otherwise.


There has been mentioned a little bit cryptic Tester\Environment::setup() call in tests. What does it do?

  • improves error dump readability (coloring included), otherwise, default PHP stack trace is printed
  • enables check that assertions has been called in test, otherwise, tests without (e.g. forgotten) assertions pass too
  • automatically starts code coverage collector when --coverage is used (described later)

Using is optional but recommended.


Could by useful to know if test is running by the Tester or manually as ordinary PHP script.

if (getenv(Tester\Environment::RUNNER)) {
    # run by Tester
} else {
    # other way


Further we will learn, that Tester runs tests in parallel in a given number of threads. We will find a thread number in an environmental variable, when we are interested:

echo "I'm running in a thread number " . getenv(Tester\Environment::THREAD).

Directories structure

It may seem prematurely to talk about it, but well-structured directories with tests saves a lot of work. We separate tests into subdirectories by namespace of tested classes:

└── tests/
    ├── NamespaceOne/
    │   ├── MyClass.getUsers.phpt
    │   ├── MyClass.setUsers.phpt
    │   └── ...
    ├── NamespaceTwo/
    │   ├── MyClass.creating.phpt
    │   ├── MyClass.dropping.phpt
    │   └── ...
    ├── test.one.phpt
    ├── test.two.phpt
    ├── ...
    └── bootstrap.php

and we create a file bootstrap.php. It contains a common code for all tests. For example classes autoloading, environment configuration, temporary directory creation, helpers and similar. Every test loads the bootstrap and pays attention to testing only. The bootstrap can look like:

require __DIR__ . '/../vendor/autoload.php';


define('TMP_DIR', '/tmp/demo-app-tests');

There is no difference to run the tests with changed directory structure. The Tester finds all *.phpt and *Test.php tests recursively and runs them:

cd demo
tester tests

But we can run tests for a single namespace easily:

tester tests/NamespaceOne


In the example test at the beginning we used two assertions: Assert::same() and Assert::exception(). Now we introduce all other types and we explain how to be used. They are methods of the Tester\Assert class but for simplification we use them further without namespace.

Assert::same($expected, $actual)

$expected must be the same as $actual. It is the same as PHP operator ===.

Assert::notSame($expected, $actual)

Opposite to Assert::same().

Assert::equal($expected, $actual)

$expected must be equal to $actual. Object identities and array keys order are ignored.

Assert::notEqual($expected, $actual)

Opposite to Assert::equal().

Assert::contains($needle, $actual)

$actual must contain $needle. If $actual is string, it must contain $needle substring. If it is an array, it must contain $needle item.

Assert::notContains($needle, $actual)

Opposite to Assert::contains().


$value must be true, so $value === true.


$value must be truthy, so $value == true.


$value must be false, so $value === false.


$value must be falsey, so $value == false.

Assert::count($count, $value)

$count must be number of elements in $value. Countable is an array or an object implementing Countable.


$value must be null, so $value === null.


$value must be Not a Number.

Assert::type($type, $value)

$value must be of given type. As $type we can use string:

  • array
  • list – same as the array but keys must start by zero and must be incremented by one
  • bool
  • callable
  • float
  • int or integer
  • null
  • object
  • resource
  • scalar
  • string
  • class name or object directly then must pass $value instanceof $type

Assert::exception($callable, $class, $message = null)

On $callable invocation an exception of $class instance must be thrown. If we pass $message, message of the exception must match (see Assert::match()). And if we pass $code, code of the exception must be the same. For example, this test fails because message of the exception does not match:

Assert::exception(function () {
    throw new App\InvalidValueException('Zero value');
}, App\InvalidValueException::class, 'Value is to low');

The Assert::exception() is unique by returning the thrown exception. So we can test a previous exception:

$e = Assert::exception(function () {
    throw new MyException('Something is wrong', 0, new RuntimeException);
}, MyException::class, 'Something is wrong');

Assert::type(RuntimeException::class, $e->getPrevious());

Assert::error($callable, $type, $message = null)

If we pass a class name as $type, this assertion behaves as absolutely same as Assert::exception().

If $type is one of E_... constants, for example E_WARNING, the $callable must generate this error when invoked. And if we pass $message message of the error must match (see Assert::match()). For example:

Assert::error(function () {
}, E_NOTICE, 'Undefined variable: i');

And the last possibility, if $type is an array the $callable must generate all expected errors. An example shows it best:

Assert::error(function () {
}, [
    [E_NOTICE, 'Undefined variable: a'],
    [E_NOTICE, 'Undefined variable: b'],


Checks that the function $callable does not throw any PHP warning/notice/error or exception. It is useful for testing a piece of code where is no other assertion.

Assert::match($pattern, $actual)

$actual must match to $pattern. We can use two pattern types:

We pass regular expression as $pattern. To delimit it we must use ~ or #. Other delimiters are not supported. For example test where $var must contain only hexadecimal digits:

Assert::match('#^[0-9a-f]$#i', $var);

The other variant is similar to string comparing but we can use some modifiers in $pattern:

  • %a% one or more of anything except the end of line characters
  • %a?% zero or more of anything except the end of line characters
  • %A% one or more of anything including the end of line characters
  • %A?% zero or more of anything including the end of line characters
  • %s% one or more white space characters except the end of line characters
  • %s?% zero or more white space characters except the end of line characters
  • %S% one or more of characters except the white space
  • %S?% zero or more of characters except the white space
  • %c% a single character of any sort (except the end of line)
  • %d% one or more digits
  • %d?% zero or more digits
  • %i% signed integer value
  • %f% floating point number
  • %h% one or more HEX digits
  • %w% one or more alphanumeric characters
  • %% one % character


# Again, hexadecimal number test
Assert::match('%h%', $var);

# Generalized path to file and line number
Assert::match('Error in file %a% on line %i%', $errorMessage);

Assert::matchFile($file, $actual)

The assertion is identical to Assert::match() but the pattern is loaded from $file. It is useful for very long strings testing. Test file stands readable.

Assert::fail($message, $actual = null, $expected = null)

This assertion always fails. It is just handy. We can optionally pass expected and actual values.

Failed assertions investigation

The Tester shows where the error is when an assertion fails. When we compare complex structures, the Tester creates dumps of compared values and saves them into directory output. For example when imaginary test Arrays.recursive.phpt fails the dumps will be saved as follows:

└── tests/
    ├── output/
    │   ├── Arrays.recursive.actual    # actual value
    │   └── Arrays.recursive.expected  # expected value
    └── Arrays.recursive.phpt          # failing test

We can change the name of the directory by Tester\Dumper::$dumpDir.

Skipping tests

Some tests can run only under certain circumstances. If these circumstances aren't met, we can skip the tests. For example, required OS type:

    Tester\Environment::skip('Requires UNIX.');

Test will be stopped and marked as s – skipped. Later we find out how to skip the test using the @skip annotation.


We have shown the simple test where assertions follow one by one at the beginning of this guide. Sometimes it is useful to enclose the assertions to test class and structure them in this way. The class must be descendant of Tester\TestCase and we talk about it simply as about testcase.

use Tester\Assert;

class GreetingTest extends Tester\TestCase
    public function testOne()

    public function testTwo()

# Run testing methods
(new GreetingTest)->run();

We can enrich a testcase by setUp() and tearDown() methods. They are called before/after every testing method:

use Tester\Assert;

class NextTest extends Tester\TestCase
    public function setUp()
        # Preparation

    public function tearDown()
        # Clean-up

    public function testOne()

    public function testTwo()

# Run testing methods
(new NextTest)->run();

Method calls order


If error occurs in a setUp() or tearDown() phase, test will fail. If error occurs in the testing method, the tearDown() method is called anyway, but with suppressed errors in it.

Annotations for TestCase methods

There are few annotations available to help us with testing methods. We write them toward the testing method.


It is equal usage of Assert::exception() inside a testing method. But notation is more readable:

 * @throws RuntimeException
public function testOne()

 * @throws LogicException  Wrong argument order
public function testTwo()


This annotation suits when we want to run the testing method multiple times but with different arguments. As argument we write method name which return parameters for testing method. The method must return an array or Traversable. Simple example:

public function getLoopArgs()
    return [
        [1, 2, 3],
        [4, 5, 6],
        [7, 8, 9],

 * @dataProvider getLoopArgs
public function testLoop($a, $b, $c)

The other annotation @dataProvider variation accepts a path to INI file (relatively to test file) as argument. The method is called so many times as the number of sections contained in INI file. File loop-args.ini:




and the method which uses the INI file:

 * @dataProvider loop-args.ini
public function testLoop($a, $b, $c)

Similarly, we can pass path to a PHP script instead of INI. It must return array or Traversable. File loop-args.php:

return [
   ['a' => 1, 'b' => 2, 'c' => 3],
   ['a' => 4, 'b' => 5, 'c' => 6],
   ['a' => 7, 'b' => 8, 'c' => 9],

Test file annotations

Following annotations are evaluated only if we run the test by the Tester, not manually as ordinary PHP script.

We write annotations at the beginning of the test file and the typing is case-insensitive. Imaginary example:

 * TEST: Basic database query test.
 * @dataProvider files/databases.ini
 * @exitCode 56
 * @phpVersion < 5.5

require __DIR__ . '/../bootstrap.php';


It is not an annotation actually. It only sets the test title which is printed on fail or into logs.


Test is skipped. It is handy for temporary tests deactivation.


Test is skipped if is not run by corresponding PHP version. We write annotation as @phpVersion [operator] version. We can leave out the operator, default is >=. Examples:

 * @phpVersion 5.3.3
 * @phpVersion < 5.5
 * @phpVersion != 5.4.5


Test is skipped if all mentioned PHP extension are not loaded. Multiple extensions can be written in a single annotation, or we can use the annotation multiple times.

 * @phpExtension pdo, pdo_pgsql, pdo_mysql
 * @phpExtension json


We write annotation as @dataProvider file.ini. INI file path is relative to the test file. Test runs as many times as number of sections contained in the INI file. Let's assume the INI file databases.ini:

dsn = "mysql:host="
user = root
password = ******

dsn = "pgsql:host=;dbname=test"
user = postgres
password = ******

dsn = "sqlite::memory:"

and the file database.phpt in the same directory:

 * @dataProvider databases.ini

$args = Tester\Environment::loadData();

The test runs three times and $args will contain values from sections mysql, postgresql or sqlite.

There is one more variation when we write annotations with a question mark as @dataProvider? file.ini. In this case test is skipped if the INI file doesn't exist.

Annotation possibilities have not been mentioned all yet. We can write conditions after the INI file. Test runs for given section only if all conditions match. Let's extend the INI file:

dsn = "mysql:host="
user = root
password = ******

[postgresql 8.4]
dsn = "pgsql:host=;dbname=test"
user = postgres
password = ******

[postgresql 9.1]
dsn = "pgsql:host=;dbname=test;port=5433"
user = postgres
password = ******

dsn = "sqlite::memory:"

and we will use annotation with condition:

 * @dataProvider  databases.ini  postgresql, >=9.0

The test runs only once for section postgresql 9.1. Other sections don’t match the conditions.

Similarly, we can pass path to a PHP script instead of INI. It must return array or Traversable. File databases.php:

return [
    'postgresql 8.4' => [
        'dsn' => '...',
        'user' => '...',

    'postgresql 9.1' => [
        'dsn' => '...',
        'user' => '...',


We write it as @multiple N where N is integer. Test runs exactly N-times.


Annotation has no parameters. We use it when we write a test as Tester\TestCase classes. In this case the test runs as many times as the number of testing methods. Every testing method runs in separated process. It can dramatically speed up the whole testing procedure. We recommend usage of this annotation.


We write it as @exitCode N where N is the exit code of the test. For example if exit(10) is called in the test, we write annotation as @exitCode 10. It is considered to be fail if the test ends with a different code. Exit code 0 (zero) is verified if we leave out the annotation


Annotation is evaluated only if PHP binary is CGI. It is ignored otherwise. We write it as @httpCode NNN where NNN is expected HTTP code. HTTP code 200 is verified if we leave out the annotation. If we write NNN as a string evaluated as zero, for example any, HTTP code is not checked at all.

@outputMatch a @outputMatchFile

The behaviour of annotations is consistent with Assert::match() and Assert::matchFile() assertions. But pattern is found in test’s standard output. A suitable use case is when we assume the test to end by fatal error and we need to verify its output.


It sets INI configuration values for test. For example we write it as @phpIni precision=20 and it works in the same way as if we passed value from the command line by parameter -d precision=20.



Tester\DomQuery class helps to test HTML or XML content. Description of all the methods is in API documentation. Here, we show basic usage:

# HTML which we want to test
$html = $template->render();

# We create DOM structure from HTML
$dom = Tester\DomQuery::fromHtml($html);

# We can test

We use CSS selector as DomQuery::has($selector) parameter.


Tester\FileMock emulates files in memory and helps you to test a code which uses functions like fopen(), file_get_contents() or parse_ini_file(). For example:

# Tested class
class Logger
    private $logFile;

    public function __construct($logFile)
        $this->logFile = $logFile;

    public function log($message)
        file_put_contents($this->logFile, $message . "\n", FILE_APPEND);

# New empty file
$file = Tester\FileMock::create('');

$logger = new Logger($file);

# Created content testing
Assert::same("Login\nLogout\n", file_get_contents($file));


Class Tester\Helpers offers method purge() which creates passed directory but only if doesn't exist yet. If so, it purges all its content. It is handy for temporary directory creation. For example in tests/bootstrap.php:

@mkdir(__DIR__ . '/tmp');  # @ - directory may already exist

define('TEMP_DIR', __DIR__ . '/tmp/' . getmypid());


Later we learn that tests run in parallel. Sometimes we need not to overlap the test running. Typically database tests need to prepare database content and they need nothing disturbs them during running time of the test. In these cases we use Tester\Environment::lock($name, $dir):

Tester\Environment::lock('database', __DIR__ . '/tmp');

The first argument is a lock name. The second one is a path to directory for saving the lock. The test which acquires the lock first runs. Other tests must wait till it is completed.


Classes or methods marked as as final are hard to test. Calling the Tester\Environment::bypassFinals() in a test beginning causes that keywords final are removed during the code loading.

require __DIR__ . '/bootstrap.php';


class MyClass extends NormallyFinalClass  # <-- NormallyFinalClass is not final anymore

Command line options

We obtain command line options overview by running the Tester without parameters or with option -h.

-p <path>

The Tester uses this PHP binary for running the tests:

tester -p /home/user/php-7.2.0-beta/php-cgi tests

-c <path>

The option is the same as for PHP binary. Passed php.ini is used for tests. No php.ini is used in default. Example when we distribute php.ini along with tests:

tester -c tests/php.ini tests


A system-wide php.ini is used. So on UNIX platform, all the /etc/php/{sapi}/conf.d/*.ini files too.

-l | –log <path>

Testing progress is written into file. All failed, skipped and also successful tests:

tester --log /var/log/tests.log tests

-d <key=value>

The option is the same as for PHP binary. It defines INI value for tests. The option can be used multiple times.


Information about skipped tests will be shown.


Tester stops testing upon the first failing test.

-j <num>

Tests run in a <num> parallel precesses. Default value is 8. If we wish to run tests in series, we use value 1.

-o <console|tap|junit|none>

Output format. Default is the console format.

  • console: the same as default, but the ASCII logo is not printed in this case
  • tap: TAP format appropriate for machine processing
  • junit: JUnit XML format, appropriate for machine processing too
  • none: nothing is printed

-w | –watch <path>

The Tester doesn't end after tests are completed but it keeps running and watching given directory. It runs tests again whenever directory changes. Parameter can be used multiple times. It is handy during tests writing and debugging.

-i | –info

It shows information about a test running environment. For example:

tester -p /usr/bin/php7.1 -c tests/php.ini --info

PHP binary:

PHP version:
7.1.7-1+0~20170711133844.5+jessie~1.gbp5284f4 (cli)

Loaded php.ini files:

PHP temporary directory:

Loaded extensions:
Core, ctype, date, dom, ereg, fileinfo, filter, hash, ...

--setup <path>

The Tester loads given PHP script on start. Variable Tester\Runner\Runner $runner is available in it. Let's assume file tests/runner-setup.php:

$runner->outputHandlers[] = new MyOutputHandler;

and we run the Tester:

tester --setup tests/runner-setup.php tests

--temp <path>

Sets a path to directory for temporary files of Tester. Default value is by sys_get_temp_dir(). When default value is not valid, you will be noticed.

If we are not sure which directory is used, we can run Tester with the --info parameter.

--colors 1|0

The Tester detects a colour-able terminal in default and colorizes its output. This option is over the autodetection. We can set colouring globally by a system environment variable NETTE_TESTER_COLORS.

--coverage <path>

Tester will generate a report with overview how much is the source code coveraged by tests. This option requires PHP extension Xdebug enabled, or PHP 7 with the PHPDBG SAPI, which is faster. The destination file extension determines the contents format. HTML or Clover XML.

tester tests --coverage coverage.html  # HTML report
tester tests --coverage coverage.xml   # Clover XML report

Extensive tests may fails during run by PHPDBG due to memory exhaustion. Coverage data collecting is memory consuming operation. In that case, calling the Tester\CodeCoverage\Collector::flush() inside a test may help. It will flush collected data into file and frees memory. When data collection is not in progress, or Xdebug is used, calling has no effect.

An example of HTML report with code coverage.

--coverage-src <path>

We use it with option --coverage simultaneously. The <path> is a path to source code for which we generate the report.

HHVM support

Till version 1.7 Tester supported HHVM 3.3.0 or newer. Support has been dropped since Tester 2.0. Usage was simple:

tester -p hhvm


  • It is not good practice to use exit() and die() to end test with a fail message. For example exit('Error in connection') ends a test with exit code 0 (zero) and it means success. Always use echo 'Error in connection' followed by exit(1).
  • Choose the most accurate assertions. For example Assert::same($a, $b), not Assert::true($a === $b). Only in the first case we get meaningful error message. In the other case we get false should be true only and it says nothing about $a and $b variables contents.
  • Use the Assert::nan() for NAN (Not a Value) testing. NAN value is very specific and assertions Assert::same() or Assert::equal() can behave unpredictably.

Own php.ini

The Tester runs PHP processes with -n option, so no php.ini is loaded (not even that from /etc/php/conf.d/*.ini in UNIX). It ensures the most clean environment for the tests run, but it also deactivates all the external PHP extensions commonly loaded by system PHP.

If you want to keep system configuration, use the -C parameter.

If you need some extensions or some special INI settings, we recommend to create own php.ini file and distribute it among the tests. Then we run Tester with -c option, e.g. tester -c tests/php.ini. The INI file may look like:




Running the Tester with a system php.ini in UNIX, e.g. tester -c /etc/php/cgi/php.ini, does not load other INI from /etc/php/conf.d/*.ini. That's the PHP behaviour, not the Tester's.

Continuous integration

If you’re not familiar with continuous integration, this guide will show you what it is all about.