Unit Testing Best Practices: Difference between revisions
From Joomla! Documentation
SniperSister (talk | contribs) Created page with "==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 ..." |
Some markup, capitalization and spelling changes. |
||
| (8 intermediate revisions by 2 users not shown) | |||
| Line 1: | Line 1: | ||
==Introduction== | == Introduction == | ||
This article lists | This article lists best practices for writing unit tests for Joomla. These practices have not been implemented in all tests of the existing test suite yet. If you find a place where this is not implemented you are invited to change it. | ||
==One | == Use Joomla's Test Case Classes == | ||
Make sure | When writing unit tests for Joomla, extend your test class from ''TestCase'' or ''[[Automated Testing With Database|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 method tests exactly one thing. This makes it easier to find the cause of a failed test, makes test methods more readable and gives more meaningful Testdox outputs (see below). | |||
Let's assume we want to test this class: | Let's assume we want to test this class: | ||
< | <syntaxhighlight lang="php"> | ||
<?php | <?php | ||
class Foo | class Foo | ||
| Line 12: | Line 15: | ||
public function bar($string) | public function bar($string) | ||
{ | { | ||
if(is_int($string)) | if (is_int($string)) | ||
{ | { | ||
return false; | return false; | ||
| Line 22: | Line 25: | ||
} | } | ||
} | } | ||
</ | </syntaxhighlight> | ||
Here's a bad example of a test for this method. It tests two things (convert to uppercase, return false on int) in the same method: | |||
< | <syntaxhighlight lang="php"> | ||
<?php | <?php | ||
class FooTest extends | class FooTest extends TestCase | ||
{ | { | ||
public function testBar() | public function testBar() | ||
| Line 37: | Line 40: | ||
} | } | ||
} | } | ||
</ | </syntaxhighlight> | ||
A better approach would be to split these two assertions into two | A better approach would be to split these two assertions into two methods: | ||
< | <syntaxhighlight lang="php"> | ||
<?php | <?php | ||
class FooTest extends | class FooTest extends TestCase | ||
{ | { | ||
public function testBar1() | public function testBar1() | ||
| Line 58: | Line 61: | ||
} | } | ||
} | } | ||
</ | </syntaxhighlight> | ||
==Use | ==Use Indicative Names for Test Methods== | ||
Indicative names for your test methods increase readability and add extra documentation to your test. | |||
Using the example above, | Using the example above, indicative method names could be: | ||
< | <syntaxhighlight lang="php"> | ||
<?php | <?php | ||
class FooTest extends | class FooTest extends TestCase | ||
{ | { | ||
public function testStringIsConvertedToUppercase() | public function testStringIsConvertedToUppercase() | ||
| Line 83: | Line 86: | ||
} | } | ||
} | } | ||
</ | </syntaxhighlight> | ||
==Use the | == Use the Most Specific Assertion Possible == | ||
PHPUnit offers a | PHPUnit offers a range of different [https://phpunit.de/manual/6.5/en/appendixes.assertions.html assertion methods]. By using the most specific one available, you reduce code and make your tests more strict. That leads to more meaningful results. Let's improve the example used above: | ||
< | <syntaxhighlight lang="php"> | ||
<?php | <?php | ||
class FooTest extends | class FooTest extends TestCase | ||
{ | { | ||
public function testStringIsConvertedToUppercase() | public function testStringIsConvertedToUppercase() | ||
| Line 96: | Line 99: | ||
$object = new foo(); | $object = new foo(); | ||
// assertSame is type safe | // assertSame is type safe. In this case, only strings are accepted. | ||
$this->assertSame('EXAMPLE', $object->bar('example')); | $this->assertSame('EXAMPLE', $object->bar('example')); | ||
} | } | ||
| Line 108: | Line 111: | ||
} | } | ||
} | } | ||
</ | </syntaxhighlight> | ||
==Run | == Run Your Tests with ''--strict'' and ''--verbose'' == | ||
By running your tests with the --strict and --verbose parameters, PHPUnit will provide | By running your tests with the ''--strict'' and ''--verbose'' parameters, PHPUnit will provide much useful information. For example: | ||
* Tests that don't make any assertions and therefor are useless | * Tests that don't make any assertions and therefor are useless | ||
* Tests that have a todo | * Tests that have a ''todo'' | ||
* Tests that are skipped | * Tests that are skipped | ||
==Generating Testdox== | == Generating Testdox == | ||
When test methods have | When test methods have indicative names, the ''--testdox'' parameter is a useful instrument to get a human-readable overview of passing and falling tests. Running our example from above with ''--testdox'' outputs: | ||
PHPUnit 4.3.1 by Sebastian Bergmann | PHPUnit 4.3.1 by Sebastian Bergmann | ||
Foo | Foo | ||
[x] String is converted to uppercase | [x] String is converted to uppercase | ||
| Line 126: | Line 128: | ||
To generate this output, PHPUnit uses the method name, strips the "test" at beginning and converts each uppercase letter into a space. | 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 test method, it's also possible to override this using the ''@testdox'' annotation in the docblock: | |||
<syntaxhighlight lang="php"> | |||
/** | |||
* @testdox Test retrieving an instance of JDocumentHTML | |||
*/ | |||
public function testRetrievingAnInstanceOfTheHtmlDocument() | |||
{ | |||
$this->assertInstanceOf('JDocumentHTML', JDocument::getInstance()); | |||
} | |||
</syntaxhighlight> | |||
'''Note''' Use this only for documentation and not while you are working on the tests. It doesn't give you any information on why a test failed. | |||
[[Category:Bug Squad]] [[Category:Development]] [[Category:Testing]] [[Category:Automated Testing]] | |||
Latest revision as of 15:27, 21 October 2022
Introduction
This article lists best practices for writing unit tests for Joomla. These practices have not been implemented in all tests of the existing test suite yet. If you find a place where this is not implemented you are invited to change it.
Use Joomla's Test Case Classes
When writing unit tests for Joomla, 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 method tests exactly one thing. This makes it easier to find the cause of a failed test, makes test methods more readable and 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;
}
}
Here's a bad example of a test for this method. It tests two 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 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 Indicative Names for Test Methods
Indicative names for your test methods increase readability and add extra documentation to your test.
Using the example above, indicative method names could be:
<?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 range of different assertion methods. By using the most specific one available, you reduce code and make your tests more strict. That leads to more meaningful results. Let's improve the example used above:
<?php
class FooTest extends TestCase
{
public function testStringIsConvertedToUppercase()
{
$object = new foo();
// assertSame is type safe. 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 much useful information. 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 indicative names, the --testdox parameter is a useful instrument to get a human-readable 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 test method, 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());
}
Note Use this only for documentation and not while you are working on the tests. It doesn't give you any information on why a test failed.