Unit Test Tutorial 1
From Joomla! Documentation
Unit Testing and Joomla![edit]
As we move forward with Joomla! Development one element that is going to be playing a larger and larger role is unit testing. My hope with this short series is to provide some basic background on unit testing, to demonstrate how essential unit tests are to stable software and to give practical instruction and examples that illustrate how to write unit tests.
As co-maintainer of the Joomla! Bug Squad, there is a question that repeatedly pops up: What is this change going to break? There are certain elements of the code base that can be rather fragile and this makes it difficult to know what things are safe to touch and what things aren't. The component routers are a good example of this. We are hesitant to touch this because many people are particular about their URLs, and experience has shown that small changes can cause significant, unforeseen problems.
Good unit tests take a great deal of the anxiety out of such changes because you can be confident that the change you are introducing is not going to have nasty, unforeseen consequences.
What Is Unit Testing?[edit]
In its purest form, unit tests are tests that are designed to ensure that small units of code behave as they are intended. Pure unit testing means that each method is tested in isolation from all other methods. That is to say, for example, that if we are writing a test for JApplication, and JApplication happens to use the JDatabase class, we want to try and design our tests such that a bug in the JDatabase class won't affect our test written for the JApplication class.
In practice, this can be difficult because sometimes class references are hard-coded. Static methods are difficult in this regard because you can't redefine classes and all tests in PHPUnit are executed in the same process.
On the other end of the scale is system testing. System tests are tests that are written to test a system as a whole. In these tests, the goal isn't to isolate particular units of code. Rather, it is to ensure that all the parts work together in the way they were intended. For web applications, these tests are often developed using a product called Selenium which allows you to control a web browser in order to simulate a user actually using your web application.
Installing PHPUnit[edit]
We are building up an extensive suite of unit tests for the Joomla! Framework. The tests are built on a unit testing framework called PHPUnit. In order to run the tests, you need to obtain PHPUnit. To get it, head over to the PHPUnit website and follow the instructions to install PHPUnit.
Running Tests[edit]
Unit tests are typically run from the command line. While this may be daunting for some, once you get the hang of things it is actually quite easy. Once you have PHPUnit installed and your path setup correctly, it is as simple as changing to the unit test directory and typing phpunit:
cd /path/to/joomla/tests/unit phpunit
After you run this command, you should see output that looks something like:
PHPUnit 3.4.11 by Sebastian Bergmann. IIIIIIIIIIIIIIIIIIIII...III..IIIIIIIIIIIIIIIIIIIIIIIIIIIIIII 60 / 2207 III...SIIIIII.IIIIIIIIIIIIII....................F.IIIIIIIIII 120 / 2207 IIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIII 180 / 2207 IIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIII 240 / 2207 IIIIIIIIIIIIIII.......................I.I...FFFFFEEEEEEEEEE. 300 / 2207 ....FFFFF.EEEIIIIIIIII...IIIFFFFF.....SSSSFSSSSSSFSSSSSSSSSS 360 / 2207 SSSSSFSS......IIF.SFFFFFFF.......I........I................. 420 / 2207 .........III.II.IIII.IIII.I.........I.IIIII.IIII..........II 480 / 2207 IIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIII 540 / 2207 IIIIIIIIIIIIIIIII.IIIIIIIIIIIIIII........................... 600 / 2207 ...IIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIII 660 / 2207 IIIIIIIIIIIIIIIIIIIIIIIIII.................................. 720 / 2207 ....................................................FFFF.... 780 / 2207 ............................................................ 840 / 2207 ............................................................ 900 / 2207 ............................................................ 960 / 2207 ............................................IIIIIIIIIIIIIIII 1020 / 2207 IIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIII 1080 / 2207 IIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIII 1140 / 2207 IIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIII 1200 / 2207 IIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIII 1260 / 2207 IIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIII 1320 / 2207 IIIIIIIIIIIIIIIIIIII...I.....II.IIIIIIIIIIIIIIIIIIIIIIIIIIII 1380 / 2207 IIIIIIIIIIIIII.IIIIIIIIIII.........IIIIII.IIIIIIIIIIIIIIIIII 1440 / 2207 IIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIII..II.............. 1500 / 2207 .........F.F...........F..FFF................II....I...F..II 1560 / 2207 IIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIII 1620 / 2207 IIIIIIIIIIIIIIIIIII...F........IIIIIIIIIIIIIIIIIIIIIIIIIIIII 1680 / 2207 IIIIIIIIIIIIII..IIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIII........... 1740 / 2207 ...F........................................................ 1800 / 2207 ............................................................ 1860 / 2207 ............................................................ 1920 / 2207 ............................................................ 1980 / 2207 ............................................................ 2040 / 2207 .........................F.................................. 2100 / 2207 ............................F............................... 2160 / 2207 ...............................F..I............ Time: 14 seconds, Memory: 89.50Mb There were 13 errors: 1) JCacheTest::testSetCaching with data set "simple" ('output', array()) Undefined property: JCacheControllerOutput::$_options 2) JCacheTest::testSetCaching with data set "complexOutput" ('output', array('', '/home/ian/www/ut_work/unittest/cache', 900, 'file')) Undefined property: JCacheControllerOutput::$_options 3) JCacheTest::testSetCaching with data set "complexPage" ('page', array('', '/home/ian/www/ut_work/unittest/cache', 900, 'file')) Undefined property: JCacheControllerPage::$_options There were 43 failures: 1) JControllerFormTest::testConstructor _asset_name has not been defined for structure Failed asserting that an array has the key <string:_asset_name>. 2) JCacheTest::testGetInstance with data set "simple" ('output', array(), 'JCacheOutput') Failed asserting that <JCacheControllerOutput> is an instance of class "JCacheOutput". 3) JCacheTest::testGetInstance with data set "complexOutput" ('output', array('', '/home/ian/www/ut_work/tests/unit/cache', 900, 'file'), 'JCacheOutput') Failed asserting that <JCacheControllerOutput> is an instance of class "JCacheOutput". 4) JCacheTest::testGetInstance with data set "complexPage" ('page', array('', '/home/ian/www/ut_work/tests/unit/cache', 900, 'file'), 'JCachePage') Failed asserting that <JCacheControllerPage> is an instance of class "JCachePage". FAILURES! Tests: 2207, Assertions: 1751, Failures: 43, Errors: 13, Incomplete: 1063, Skipped: 29.
There are many situations where you may not want to run the whole test suite. See the Running Unit Tests page for details on how to be selective with the tests that you run.
Anatomy of a Test[edit]
Unit tests are merely small chunks of PHP code that:
- Setup certain preconditions for the code to be tested (inputs).
- Execute the code to be tested.
- Tests assertions about the results (outputs).
We will see these basic elements in each of the examples below.
Writing Our First Test[edit]
The easiest and most simple type of unit test to write is a test for code that is stateless. That is, a test for which the return value of the method depends solely on the inputs to the method. Most of the basic PHP functions are examples of stateless functions. The strtoupper function, for example, takes one parameter – a string – and returns a string value. You can easily create a set of inputs and corresponding outputs and write a test.
Examples in the Joomla! Framework include much of the JArrayHelper class. Most of these methods are static and don't depend on anything except the data passed to the method. For this example, we will use the JArrayHelper::getColumn method. This method takes two parameters and returns an array. Following the three steps outlined above, we break things down into three elements:
1. Setup Preconditions[edit]
In this case, there are no preconditions because our method does not depend on anything other than the parameters that are passed to it. Our only preconditions are the parameters that we pass to the method.
2. Execute the Code to be Tested[edit]
Our method in question will take an array of associative arrays or objects and return an array of the values of the specified index. To test our method, we need to create an array of associative arrays:
$test_array = array(
array(
'sport' => 'football',
'teams' => '16',
'country' => 'United States'
).
array(
'sport' => 'badminton',
'teams' => '12',
'country' => 'Germany'
),
array(
'sport' => 'basketball',
'teams' => '20',
'country' => 'Canada'
)
);
Now we execute the method and retrieve the results:
$result_array = JArrayHelper::getColumn($test_array, 'country');
We have now executed the method we want to test and examine the results.
3. Tests Assertions About the Results (Outputs)[edit]
Since this method is fairly simple, testing the results is also simple. We need to figure out what we expect to get back from the method. In this case, we expect to get an array containing an array of the countries.
$this->assertThat(
$result_array,
$this->equalTo(array('United States', 'Germany', 'Canada')),
'We did not get the proper column data back'
);
Our final test looks like:
public function testGetColumnTutorial()
{
$test_array = array(
array(
'sport' => 'football',
'teams' => '16',
'country' => 'United States'
),
array(
'sport' => 'badminton',
'teams' => '12',
'country' => 'Germany'
),
array(
'sport' => 'basketball',
'teams' => '20',
'country' => 'Canada'
)
);
$result_array = JArrayHelper::getColumn($test_array, 'country');
$this->assertThat(
$result_array,
$this->equalTo(
array('United States', 'Germany', 'Canada')
),
'We did not get the proper column data back'
);
}
Unit Test Tutorial 2[edit]
This article is continued here: Unit Test Tutorial 2. This includes looking into the derived 'PHPUnit_Framework_TestCase' testclass and the minimum requirements for a standalone test class.