What is Unit Testing?
The term, quite simply, refers to the process of dividing software into the smallest testable components, called units, that can be tested independently to assert whether they fulfill their requirements and operate as intended.
There are many benefits that come with unit testing. One of the most important is that it allows you (and perhaps even your stakeholders) to determine the completeness of your software. By simply running your tests cases, you can easily confirm that the code works as expected.
For a developer, test cases can prove to be invaluable, allowing the developer to:
- prove that their code works as intended before committing
- determine whether recent changes have caused problems
- observe the behavior of the software when it deals with abnormalities
- plan work by writing test cases for unimplemented units (this is known as test-driven development)
- automate the testing process
This of course isn’t without it downsides. Test cases take time to write especially for existing code. Arguably, this is countered with the time saved later on but it’s fair to say that it does pause development slightly in the early stages. Furthermore, writing test cases for existing software can be a tedious task which is often not feasible.
There is also the case of human error. A test case can have bugs too which can cause it to wrongly assert that a unit works. For this reason, it is not recommended to relying completely on the unit testing. Having someone review your code and performing manual testing (where applicable) can save your skin!
Setting up
Now that we’re past the lecturing part of the article, let’s begin by making sure you have everything set up.
PHPUnit
You can download PHPUnit from here for a manual install. Alternatively, if you are using Composer you can simply add PHPUnit as a dependency – just make sure you add it as a “dev” dependency!
It is also important to download the correct version, depending on which PHP version you want to develop on. The PHPUnit site should indicate which PHPUnit version supports which PHP version.
You should now have downloaded a phar file.
Linux, Unix and OSX
Nix systems make it easy. Just open a terminal at the directory where the phar file was downloaded, and execute the following commands:
1
2
|
chmod +x phpunit.phar
sudo mv phpunit.phar /usr/local/bin/phpunit
|
The above commands make the phar file executable and move it to the proper directory. To check if it’s installed correctly, run the following command from a different directory:
1
|
phpunit --version
|
Windows
If you are on Windows, move the downloaded file into a directory that is in your PATH, or create a new directory and add it to your PATH. In my case, I have the C:\bin directory where I keep all of my custom executables.
You’ll also need to create a batch file since Windows cannot be made to execute any file like nix systems. Create the file phpunit.bat in the same directory near phpunit.phar. Open the batch file in a text editor and paste in the following:
1
2
3
|
@echo off
set PHPBIN="C:\php\php.exe"
"%PHPBIN%" -d safe_mode=Off "C:\bin\phpunit.phar" %*
|
There are a couple of things you’ll need to change here.
On the second line, you’ll need to change “C:\php\php.exe” to the correct path to your PHP executable. You can also use the executable that comes with your local server (such as XAMPP) if you wish.
On the last line, you’ll need to change “C:\bin\phpunit.phar” to the correct path to the phar file that you’ve downloaded.
To check if it’s installed correctly, run the following command. Make sure that you can run this command from any directory.
1
|
phpunit --version
|
If it’s not working, try opening a new Command Prompt. The console only reads your PATH variable when it’s launched, so you’ll need to close and reopen it when you make changes to your PATH.
Writing Test Cases
Consider the following class:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
|
/**
* A simple class with a numeric counter, allowing only positive addition.
*/
class Counter
{
/**
* Internal counter.
*
* @var integer
*/
protected $_c;
/**
* Constructs a new instance.
*
* @param integer $i [optional] Initial value. Default: 0
*/
public function __construct($i = 0)
{
$this->_c = $i;
}
/**
* Adds a number to this counter.
* Strings and floats will be casted into integers.
*
* @param integer $x The number to add.
*/
public function add($x)
{
$a = intval($x);
if ($a < 0) {
$a = 0;
}
$this->_c += $a;
}
/**
* Gets the counter value.
*
* @return integer The internal counter value.
*/
public function get()
{
return $this->_c;
}
}
|
A class with an internal number that can have positive numbers added to it. Doesn’t look like such a simple class could have bugs, right? You’d be surprised – look closely and you’ll find one!
Let’s create a test case for it:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
|
class CounterTest extends PHPUnit_Framework_TestCase
{
/**
* The instance used for each test.
*
* @var Counter
*/
protected $counter;
/**
* This method is called before a test is executed.
*/
protected function setUp()
{
$this->counter = new Counter(0);
}
/**
* This method is called after a test is executed.
*/
protected function tearDown()
{
}
/**
* Tests the get method.
*
* @covers Counter::get()
*/
public function testGet()
{
assertEquals(0, $this->counter->get());
}
/**
* Tests the add method with a positive integer parameter.
*
* @covers Counter::add()
*/
public function testAdd()
{
$this->counter->add(6);
$this->assertEquals(6, $this->counter->get());
}
/**
* Tests the add method with a negative integer parameter.
*
* @covers Counter::add()
*/
public function testAddNegative()
{
$this->counter->add(-3);
$this->assertEquals(0, $this->counter->get());
}
/**
* Tests the add method with a string parameter.
*
* @covers Counter::add()
*/
public function testAddNumericString()
{
$this->counter->add("12");
$this->assertEquals(12, $this->counter->get());
}
/**
* Tests the add method with an invalid string parameter.
*
* @covers Counter::add()
*/
public function testAddInvalidString()
{
$this->counter->add("invalid");
$this->assertEquals(0, $this->counter->get());
}
/**
* Tests the add method with a float parameter.
*
* @covers Counter::add()
*/
public function testAddFloat()
{
$this->counter->add(5.98);
$this->assertEquals(5, $this->counter->get());
}
}
|
One thing to note: this test case is not complete! For one thing, we’re not testing the constructor, which allows negative integers to be passed. We could also have added tests to check how the Counter::add() method works with array parameters, negative string parameters, etc.
However, even though the above test is over simplified, is demonstrates all the basics of unit testing. Firstly, notice how the test case extends the PHPUnit_Framework_TestCase class from PHPUnit. The first two methods are actually being overridden from that class and are used to set up instances for our test and perform any cleanup after they’re done. From the rest of the methods in the class, PHPUnit will look for those that begin with the prefix “test” and treat them as tests.
Also notice how the methods that test the Counter::add() method each only cover one type of input. This way, if a test fails we’ll know exactly what caused it without having to debug further or guess.
These tests will also allow us to ensure that the class produces the same results as per its requirements, even if the code were to change. For example, let’s say we performed the following modifications:
1
2
3
4
5
6
7
8
9
10
|
public function __construct($i = 0)
{
$this->_c = 0;
$this->add($i);
}
public function add($x)
{
$this->_c += max(0, intval($x));
}
|
Remember when I said that this class was too simple to have bugs? Even if that is true, the test cases can help us to safely assert whether this new logic works the same as the previous. Unit tests are not concerned with the implementation; as long as the results match the expected values the tests will pass. Besides, it wouldn’t be my first time confusing min() and max() .
Test-driven Development
Recall how in the last section we created the Counter class and then created a test case for it. Test-driven development is exactly that but the other way around. Initiating development via test cases is similar to having product requirements. The test cases will describe how a particular unit should behave and what we’re expecting.
Using the previous Counter example, we’d first start by writing the class, except without any logical code and perhaps even without properties. For instance, the Counter::get() and Counter::add() methods can simply return false . The second step would be to create the test case.
At first, all the tests will fail – which is understandable. From here, we continue the development process by implementing the logic, piece by piece. Perhaps we’ll start with the Counter::_c property and make the Counter::get() method return it. If we execute the test case at this point, some of the tests should pass. Next we’ll make the Counter::add() method simply add the parameter. If we execute the test case again, more tests will pass.
Seeing a pattern? Slowly implementing the logic to fulfill the tests is the foundation of test-driven development as it ensures that every bit of our code is being accounted for in its own test.
We can take things a step further and create an interface:
1
2
3
4
5
|
interface CounterInterface
{
public function add($x);
public function get();
}
|
This interface helps us to define what to expect when using objects that implement it. For unit testing, this can help us make use of any implementation by making the test case abstract.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
|
abstract class CounterInterfaceTest extends PHPUnit_Framework_TestCase
{
/**
* @var CounterInterface
*/
protected $counter;
/**
* @return CounterInterface
*/
abstract public function getObject();
/**
* This method is called before a test is executed.
*/
protected function setUp()
{
$this->counter = $this->getObject();
}
/**
* Just a useful shorthand method call to check if the counter is positive.
*/
public function assertCounterPositive(CounterInterface $counter)
{
$this->assertGreaterThan(0, $counter->get());
}
// ... test methods
}
|
Firstly, the test is declared abstract and named CounterInterfaceTest to signify that the test case is targeted for an interface and is meant to be extended, since an interface needs an implementation to be used for testing.
Secondly, we’ve added the CounterInterfaceTest::getObject() abstract method, which should return a new instance of an object that implements CounterInterface. This way, our abstract test case does not mention any concrete implementation class – only the interface – with the test methods for the Counter::get() and Counter::add() methods written only once in this abstract test case.
Finally, the CounterInterfaceTest::assertCounterPositive(CounterInterface $counter) method is just there to avoid repeated assertions to check if the counter is positive.
Learn more
This article is only meant to serve as an introduction. The topic of testing is quite broad, with mock objects coming into play and continuous integration taking your game to the next level.
This article only outlines unit testing with PHPUnit. There are other unit testing libraries available, such as Netter Tester and atoum, as well as other important techniques that deserve to be mentioned, like Skelgen to auto-generate test cases, mock objects to mock your database and continuous integration to take your game to the next level.
There are also other forms of testing that can help serve different purposes. For instance, acceptance testing is used to determine if the system meets a set of requirements. More specifically, user acceptance testing is used to verify that the system works as expected for your users.
You may also wish to look into automating your testing further through the use of technologies such as the Selenium WebDriver, which provides an API to allow your tests to control the browser for tasks such as filling in fields, clicking links and checking the contents of the DOM.
I’ll leave the rest to you. Happy testing!
0 comments:
Post a Comment