Writing Tests
Writing tests for Nette Tester is unique in that each test is a PHP script that can be run standalone.. This has great potential. As you write the test, you can simply run it to see if it works properly. If not, you can easily step through in the IDE and look for a bug.
You can even open the test in a browser. But above all – by running it, you will perform the test. You will immediately find out if it passed or failed.
In the introductory chapter, we showed a really trivial test of using PHP array. Now we will create our own class, which we will test, although it will also be simple.
Let's start with a typical directory layout for a library or project. It is important to separate the tests from the rest of the code, for example due to deployment, because we do not want to upload tests to server. The structure may be as follows:
├── src/ # code that we will test
│ ├── Rectangle.php
│ └── ...
├── tests/ # tests
│ ├── bootstrap.php
│ ├── RectangleTest.php
│ └── ...
├── vendor/
└── composer.json
And now we will create individual files. We will start with the tested class, which we will place in the
file src/Rectangle.php
<?php
class Rectangle
{
private float $width;
private float $height;
public function __construct(float $width, float $height)
{
if ($width < 0 || $height < 0) {
throw new InvalidArgumentException('The dimension must not be negative.');
}
$this->width = $width;
$this->height = $height;
}
public function getArea(): float
{
return $this->width * $this->height;
}
public function isSquare(): bool
{
return $this->width === $this->height;
}
}
And we'll create a test for it. The name of the test file should match mask *Test.php
or *.phpt
, we
will choose the variant RectangleTest.php
:
<?php
use Tester\Assert;
require __DIR__ . '/bootstrap.php';
// general oblong
$rect = new Rectangle(10, 20);
Assert::same(200.0, $rect->getArea()); # we will verify the expected results
Assert::false($rect->isSquare());
As you can see, assertion methods such as Assert::same()
are used to assert that an
actual value matches an expected value.
The last step is to create 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:
<?php
require __DIR__ . '/vendor/autoload.php'; # load Composer autoloader
Tester\Environment::setup(); # initialization of Nette Tester
// and other configurations (just an example, in our case they are not needed)
date_default_timezone_set('Europe/Prague');
define('TmpDir', '/tmp/app-tests');
This bootstrap assumes that the Composer autoloader will be able to load the class Rectangle.php
as
well. This can be achieved, for example, by setting the autoload section
in composer.json
, etc.
We can now run the test from the command line like any other standalone PHP script. The first run will reveal any syntax errors, and if you didn't make a typo, you will see:
$ php RectangleTest.php
OK
If we change in the test the statement to false Assert::same(123, $rect->getArea());
, this will happen:
$ php RectangleTest.php Failed: 200.0 should be 123 in RectangleTest.php(5) Assert::same(123, $rect->getArea()); FAILURE
When writing tests, it is good to catch all extreme situations. For example, if the input is zero, a negative number, in other cases an empty string, null, etc. In fact, it forces you to think and decide how the code should behave in such situations. The tests then fix the behavior.
In our case, a negative value should throw an exception, which we verify with Assert::exception():
// the width must not be negative number
Assert::exception(
fn() => new Rectangle(-1, 20),
InvalidArgumentException::class,
'The dimension must not be negative.',
);
And we add a similar test for height. Finally, we test that isSquare()
returns true
if both
dimensions are the same. Try to write such tests as an exercise.
Well-Arranged Tests
The size of the test file can increase and quickly become cluttered. Therefore, it is practical to group individual tested areas into separate functions.
First, we will show a simpler but elegant variant, using the global function test()
. The tester doesn't create it
automatically, to avoid a collision if you had a function with the same name in your code. It is only created by the
setupFunctions()
method, which you call in the bootstrap.php
file:
Tester\Environment::setup();
Tester\Environment::setupFunctions();
Using this function, we can nicely divide the test file into named units. When executed, labels will be displayed one after the other.
<?php
use Tester\Assert;
require __DIR__ . '/bootstrap.php';
test('general oblong', function () {
$rect = new Rectangle(10, 20);
Assert::same(200.0, $rect->getArea());
Assert::false($rect->isSquare());
});
test('general square', function () {
$rect = new Rectangle(5, 5);
Assert::same(25.0, $rect->getArea());
Assert::true($rect->isSquare());
});
test('dimensions must not be negative', function () {
Assert::exception(
fn() => new Rectangle(-1, 20),
InvalidArgumentException::class,
);
Assert::exception(
fn() => new Rectangle(10, -1),
InvalidArgumentException::class,
);
});
If you need to run the code before or after each test, pass it to setUp()
or tearDown()
:
setUp(function () {
// initialization code to run before each test()
});
The second variant is object. We will create the so-called TestCase, which is a class where individual units are represented by methods whose names begin with test–.
class RectangleTest extends Tester\TestCase
{
public function testGeneralOblong()
{
$rect = new Rectangle(10, 20);
Assert::same(200.0, $rect->getArea());
Assert::false($rect->isSquare());
}
public function testGeneralSquare()
{
$rect = new Rectangle(5, 5);
Assert::same(25.0, $rect->getArea());
Assert::true($rect->isSquare());
}
/** @throws InvalidArgumentException */
public function testWidthMustNotBeNegative()
{
$rect = new Rectangle(-1, 20);
}
/** @throws InvalidArgumentException */
public function testHeightMustNotBeNegative()
{
$rect = new Rectangle(10, -1);
}
}
// Run test methods
(new RectangleTest)->run();
This time we used an annotation @throw
to test for exceptions. See the TestCase
chapter for more information.
Helpers Functions
Nette Tester includes several classes and functions that can make testing easier for you, for example, helpers to test the content of an HTML document, to test the functions of working with files, and so on.
You can find a description of them on the page Helpers.
Annotation and Skipping Tests
Test execution can be affected by annotations in the phpDoc comment at the beginning of the file. For example, it might look like this:
/**
* @phpExtension pdo, pdo_pgsql
* @phpVersion >= 7.2
*/
The annotations say that the test should only be run with PHP version 7.2 or higher and if the PHP extensions pdo and
pdo_pgsql are present. These annotations are controlled by command line test runner, which, if the
conditions are not met, skips the test and marks it with the letter s
– skipped. However, they have no effect when
the test is run manually.
For a description of annotations, see Test Annotations.
The test can also be skipped based on own condition with Environment::skip()
. For example, we will skip this test
on Windows:
if (defined('PHP_WINDOWS_VERSION_BUILD')) {
Tester\Environment::skip('Requires UNIX.');
}
Directory Structure
For only slightly larger libraries or projects, we recommend dividing the test directory into subdirectories according to the namespace of the tested class:
└── tests/
├── NamespaceOne/
│ ├── MyClass.getUsers.phpt
│ ├── MyClass.setUsers.phpt
│ └── ...
│
├── NamespaceTwo/
│ ├── MyClass.creating.phpt
│ ├── MyClass.dropping.phpt
│ └── ...
│
├── bootstrap.php
└── ...
You will be able to run tests from a single namespace ie subdirectory:
tester tests/NamespaceOne
Edge Cases
A test that does not call any assertion method is suspicious and will be evaluated as erroneous:
Error: This test forgets to execute an assertion.
If the test without calling assertions is really to be considered valid, call for example Assert::true(true)
.
It can also be treacherous to use exit()
and die()
to end the test with an error message. For
example, exit('Error in connection')
ends the test with a exit code 0, which signals success. Use
Assert::fail('Error in connection')
.