Unit Testing Best Practices

From Joomla! Documentation

Introduction

This article lists a few best practices related to writing unit tests for Joomla. These best practices have not been implemented in all tests of the existing testsuite yet, so if you find a place where this is not implemented in this way consider to change it and implement it.

Use Joomla's Test Case Classes

When writing unit tests for Joomla, make sure to extend your test class from TestCase or TestCaseDatabase instead the default PHPUnit_Framework_Testcase. The Joomla cases provide a few ready to use mock objects and also provide some special exception handling related to the legacy JError classes.

One test per method

Make sure, that each test method tests exactly one thing. This makes it much easier to find out what causes a failed test, makes test methods more readable and also gives more meaningful Testdox outputs (see below).

Let's assume we want to test this class:

<?php
class Foo
{
	public function bar($string)
	{
		if (is_int($string))
		{
			return false;
		}

		$string = strtoupper($string);

		return $string;
	}
}

Below you can find a bad example of a test for this method, because it tests two different things (convert to uppercase, return false on int) in the same method:

<?php
class FooTest extends TestCase
{
	public function testBar()
	{
		$object = new foo();

		$this->assertEquals('EXAMPLE', $object->bar('example'));
		$this->assertEquals(false, $object->bar(4));
	}
}

A better approach would be to split these two assertions into two test methods:

<?php
class FooTest extends TestCase
{
	public function testBar1()
	{
		$object = new foo();

		$this->assertEquals('EXAMPLE', $object->bar('example'));
	}

	public function testBar2()
	{
		$object = new foo();

		$this->assertEquals(false, $object->bar(4));
	}
}

Use meaningful names for test methods

Meaningful names for your test methods increase readability and add a bit of extra documentation to your test. Meaningful tests also give a really nice output when using the --testdox option of PHPUnit (see below).

Using the example above, meaningful method names could look like this:

<?php
class FooTest extends TestCase
{
	public function testStringIsConvertedToUppercase()
	{
		$object = new foo();

		$this->assertEquals('EXAMPLE', $object->bar('example'));
	}

	public function testFalseIsReturnedWhenIntIsUsedAsArgument()
	{
		$object = new foo();

		$this->assertEquals(false, $object->bar(4));
	}
}

Use the most specific assertion possible

PHPUnit offers a wide range of different assertion methods. By using the most specific one that's available, you reduce code in your tests and also make your tests more strict which leads to more meaningful results. Again, let's improve the example used above:

<?php
class FooTest extends TestCase
{
	public function testStringIsConvertedToUppercase()
	{
		$object = new foo();

		// assertSame is type safe, so in this case only strings are accepted
		$this->assertSame('EXAMPLE', $object->bar('example'));
	}

	public function testFalseIsReturnedWhenIntIsUsedAsArgument()
	{
		$object = new foo();

		// reduced code
		$this->assertFalse($object->bar(4));
	}
}

Run your tests with --strict and --verbose

By running your tests with the --strict and --verbose parameters, PHPUnit will provide you a lot of useful informations, for example:

  • Tests that don't make any assertions and therefor are useless
  • Tests that have a todo
  • Tests that are skipped

Generating Testdox

When test methods have meaningful names, the --testdox parameter is a very useful instrument to get a human readble, nicely looking overview of passing and falling tests. Running our example from above with --testdox outputs:

PHPUnit 4.3.1 by Sebastian Bergmann.

Foo
 [x] String is converted to uppercase
 [x] False is returned when int is used as argument

To generate this output, PHPUnit uses the method name, strips the "test" at beginning and converts each uppercase letter into a space.

If you want to override the default testdoc generated for a testmethod, it's also possible to override this using the @testdox annotation in the docblock:

/**
 * @testdox  Test retrieving an instance of JDocumentHTML
 */
public function testRetrievingAnInstanceOfTheHtmlDocument()
{
	$this->assertInstanceOf('JDocumentHTML', JDocument::getInstance());
}

Attention: Use this only for documentation and not while you are working on the tests, because it doesn't give you any information why a test failed.