Lesson 2 - Introduction to unit tests in PHP and PHPUnit installation
In the previous lesson, Introduction to web application testing in PHP, we made a relatively solid introduction to the issue. We also introduced the v-model that illustrates the relationship between the individual design phase outputs and the relevant tests.
We always write tests based on design, not the implementation. In other words, we create them based on the expected functionality. It can be obtained either directly from the customer (and that's in the case of acceptance tests) or from the programmer (architect), where he specifies how different methods should behave. Today, we're going to focus on these tests, that are called unit tests, and which test the detailed specification of the application, thus its classes.
Remember that we never write tests according to how something is programmed inside! It could very easily seduce our thinking in just one way, and we could forget that the method might get some other inputs which it isn't expecting. In fact, testing isn't related with implementation at all, we always test whether the requirements are met.
Which classes to test
Unit tests test individual methods in classes. For sure, I repeat that it doesn't make much sense to test single-purpose methods, for example, in models that just pick something from the database. To be more specific, there is no point in testing a method like this:
public function insertItem($name, $price) { $this->db->query("INSERT INTO item (name, price) VALUES (?, ?)", $name, $price); }
The method inserts an item to the database. Typically, it's only used in some form, and if it doesn't work, it'll be detected by acceptance tests as the new item wouldn't appear on a list. There is a lot of the similar methods in applications and we would unnecessarily waste our time covering something we can easily cover in other tests.
Unit tests are most often found with libraries, the tools the programmer uses in many places or even in multiple projects and should be 100% functional. You may remember when you used a library downloaded, for example, from GitHub. Most likely, there were also tests included, which were probably located in the "tests" folder next to the "src" folder in the project's directory structure. For example, if we write an application in which we often need some mathematical calculations, such as factorials and other probability functions, it's a good practice to create a library for these calculations, and also it's a good idea to cover such library by tests.
Example
As you probably expected, we're going to create a similar class and try to test it. Not to waste our time, we'll just create a simple calculator that will be able to:
- sum
- subtract
- multiply
- divide
Creating the project
In reality, there would be some more complicated calculations in the class,
but we won't deal with it here. Create a new project called
calculator
and add the Calculator
class with the
following implementation:
<?php /** * Represents a simple calculator */ class Calculator { /** * Sums up 2 numbers * @param int|float $a The first number * @param int|float $b The second number * @return int|float The sum of the numbers */ public function sum($a, $b) { return $a + $b; } /** * Subtracts 2 numbers * @param int|float $a The first number * @param int|float $b The second number * @return int|float The difference of the numbers */ public function subtract($a, $b) { return $a - $b; } /** * Multiplies 2 numbers * @param int|float $a The first number * @param int|float $b The second number * @return int|float The product of the numbers */ public function multiply($a, $b) { return $a * $b; } /** * Divides 2 numbers * @param int|float $a The first number * @param int|float $b The second number * @return int|float The quotient of the numbers */ public function divide($a, $b) { if ($b == 0) throw new \InvalidArgumentException("Cannot divide by zero!"); return $a / $b; } }
The interesting thing in the code is just the divide()
method,
which throws an exception if we divide by zero. The default PHP behavior is
causing a script error, which the user should never see in the application. The
class could be even in a namespace, we'd import it later in the tests by the
use
keyword.
PHPUnit
In PHP, unit tests are mostly written in the PHPUnit framework, which each PHP programmer should know. There are, of course, alternative tools, such as Nette tester, which all work very similarly.
While PHPUnit can be installed separately, both here in the course and also in your own applications, we will later need other types of tests, at least the acceptance tests. Because of them, you need to work with other tools and to install everything separately would create quite a lot of work. Therefore, install PHPUnit using the Codeception framework.
Codeception
Codeception is a comprehensive test framework for PHP that includes:
- PHPUnit
- Acceptance package for selenium
- Another testing frameworks, insignificant for us now
We can install it either via a composer or by simply downloading a single .phar file. If you have not heard of .phar files yet, they are executable archives with PHP applications that can be run e.g. by your IDE.
Installation
I'll use the first option via the .phar file here, the installation via Composer can be seen below in case you're interested in it. Go to the quickstart web page at http://codeception.com/quickstart and download the codecept.phar file, ideally save it to the folder with today's project.
These articles are tested for Codeception version 2.5.6. This version can be downloaded here: https://codeception.com/builds (just click on the Download latest 2.5 Release link).
Since the tests are an advanced topic, we'll use the more advanced IDE - PhpStorm. Of course, you can also do testing with NetBeans if you want for some reason.
Now we create an alias for the .phar file, so we can simply run it from the console. In the File menu, choose Settings and type "Command Li" into the search box above to open the Command Line Tool Support tool.
Use the "+" button at the top right to add a new "Custom tool" item with visibility for this project. Then fill in the form:
- Tool path:
C:\xampp\php\php.exe codecept.phar
, you can eventually modify the path to your PHP interpreter. On Linux, just enter the php instead. - Alias: test (that's the name of the command through which we'll run the archive)
Confirm all windows.
Installation via Composer
This passage describes how to install Codeception using Composer. If you
don't use it, skip it. If you have Composer, you can install Codeception using
the composer require "codeception/codeception" --dev
command.
Articles are tested using Codeception version 2.5.6 which was
the latest version when we were writing them. This specific version can be
downloaded using the following command
composer require "codeception/codeception:2.5.6"
.
I recommend linking PhpStorm with Composer and Codeception, otherwise the code autocompletion might not work.
The IDE should be set automatically, however, I provide screenshots of my settings to be sure:
In this case, the Composer command path will be set as the path to the batch
file generated by Composer. It should look like this:
project_path\Calculator\vendor\bin\codecept.bat
. Use the file
without the .bat
extension on Linux.
Bootstrap
Now click on the menu Tools -> Run command and enter the following code into the console (be careful not to mess it up with the terminal, which PhpStorm also contains):
test bootstrap
This will generate tests into our project.
Notice that the
tests' folder has been added, which contains several other files and subfolders. The `unit
subfolder will be important to us so far, we'll generate new unit tests into it.
Because tests use classes from our application, they need us to define at least
the autoloader. This is done in _bootstrap.php
files, which are
here either separate for each type of test or for all tests together.
In our case, we'll create a _bootstrap.php
in the
unit
folder, where we'll define a simple autoloader:
<?php function autoloader($class_name) { if (!file_exists(__DIR__ . '/../../' . $class_name . '.php')) return false; require(__DIR__ . '/../../' . $class_name . '.php'); } spl_autoload_register('autoloader');
Notice that the autoloader returns false
if it fails to load the
class. This is very important, because this process can be followed by other
Codeception autoloaders that are used for its own files.
It can happen that these files won't be enabled by default. Be sure to check
the codeception.yml
file located in the parent folder of the tests
folder. It should contain the following lines:
settings: bootstrap: _bootstrap.php
Let me show you how mine file looks like for the completness' sake:
paths: tests: tests output: tests/_output data: tests/_data support: tests/_support envs: tests/_envs actor_suffix: Tester settings: bootstrap: _bootstrap.php extensions: enabled: - Codeception\Extension\RunFailed
Generating tests
We generate a new unit test using a console command as well:
test generate:test unit CalculatorTest
The test name is usually assembled from the name of the test class + the word "Test", in our case, "CalculatorTest". In the command, always edit the test name by the name of the class you are testing.
If you omit the word "Test" at the end of the name, it'll be
added automatically. So the following command can also be used:
test generate:test unit Calculator
and the result will be the
same.
If you ever needed to generate tests into another folder (for example, you
have multiple tests
in one project, one in the app
folder and the other in vendor
), you can specify it as follows:
test --config=vendor/SomeFrameworkWithTestsFolder generate:test unit SomeTest
The --config
parameter specifies the path to the
folder that contains the codeception.yml
configuration file, so the
folder must contain tests (a structure generated by the bootstrap
command). This allows us to generate and run tests of different submodules of
the system.
In the unit folder, a new file with the following code has been generated:
<?php class CalculatorTest extends \Codeception\Test\Unit { /** * @var \UnitTester */ protected $tester; protected function _before() { } protected function _after() { } // tests public function testSomeFeature() { } }
Alternatively, you can have a version of PHPUnit that uses namespaces, then it'll inherit from a class named just TestCase and above it, there'll be import from the appropriate space.
It's probably not surprising that the (scenario) class test is also represented by the class and individual tests by the methods What is more interesting is the fact that we can find the pre-prepared methods there. The last one, with the "test" word at the beginning, will be started automatically as every "test" method is. We will explain these other two methods in the next lesson, PHP Testing - Finishing Unit Tests.