452 lines
19 KiB
Markdown
452 lines
19 KiB
Markdown
|
# General testing guidelines
|
||
|
|
||
|
## The short summary
|
||
|
|
||
|
This list summarizes what will be described in the next few chapters:
|
||
|
|
||
|
- You really should write tests for your code
|
||
|
- Think about your what you want to test and how you can assert the behaviour correctly.
|
||
|
- Isolate your tests and start without any assumptions about your test environment (like a specific user existing). The
|
||
|
only excuse here is to assume a correct login if you need a database for testing (but not the tables/content of the database!)
|
||
|
- Don't just test correct behaviour - test border cases and invalid input or assumptions to make sure the application handles
|
||
|
cases that might not occur in a normal test environment
|
||
|
- Use description strings in your assertions, this makes it easy to detect what's going wrong when they fail and it's easier
|
||
|
to follow what *exactly* you are asserting
|
||
|
- Test methods should be one scenario and the method name should describe this scenario.
|
||
|
*testLogin* is bad for example (do you test if the login fails? Or if it is correct?
|
||
|
*testLoginWithCorrectCredentials*, *testLoginWithWrongPassword* is a far better name here
|
||
|
- Your assertions should reflect one test scenario, i.e. don't write one test method that tests if something works **and**
|
||
|
if it correctly detects errors after it works. Write one test to determine the behaviour with correct input and one that
|
||
|
tests the behaviour with invalid input.
|
||
|
- When writing unit-tests (like function level tests), try to keep your dependencies as low as possible (best indicator herefor
|
||
|
is the number of require calls in your test). Mock external components and inject them into the class you want to test. If
|
||
|
your testsubject is not able to use mocked dependencies, it's often a design flaw and should be considered as a bug
|
||
|
(and be fixed)
|
||
|
- When writing component tests with a lot of dependencies, wrap the require calls in a LibraryLoader subclass
|
||
|
|
||
|
|
||
|
## What should be tested
|
||
|
|
||
|
### Writing meaningful tests
|
||
|
|
||
|
Writing tests doesn't ensure that your code is free of errors, but it can test if, in a specific scenario, the behaviour of
|
||
|
your code is as expected. This means that you have to think about your test scenario before writing a test. This involves
|
||
|
three steps:
|
||
|
|
||
|
- Determine what you want to test: Which errors can occur, what is the normal input and what are the border cases.
|
||
|
- Define a test scenario: What datasets make sense for the test?
|
||
|
- How do I write my assertions so they are really meaningful about the correctness of the testcase
|
||
|
|
||
|
Especially the border cases are important, often they lay inside of try/catch blocks or error detection routines. When
|
||
|
looking at your code coverages, these blocks should be covered.
|
||
|
|
||
|
### Example
|
||
|
|
||
|
Let's say you have the following function (the examples in this section are to be considered as php-like pseudocode) :
|
||
|
|
||
|
function isValidName($name)
|
||
|
{
|
||
|
if (hasCorrectFormat($name)) {
|
||
|
return false;
|
||
|
}
|
||
|
|
||
|
if (nameExistsInDatabase($name)) {
|
||
|
return false;
|
||
|
}
|
||
|
|
||
|
return true;
|
||
|
}
|
||
|
|
||
|
|
||
|
#### Determine what to test:
|
||
|
|
||
|
At first glance there can be 3 scenarios:
|
||
|
|
||
|
1. The username is unique and valid
|
||
|
2. The username has the wrong format
|
||
|
3. The username has the correct format, but exists in the database
|
||
|
|
||
|
But - what happens if the database is down? Should an exception be thrown or should it return false? This case has to be added
|
||
|
to your tests, so you have (at least!) the following scenarios:
|
||
|
|
||
|
1. The username is unique and valid
|
||
|
2. The username has the wrong format
|
||
|
3. The username has the correct format, but exists in the database
|
||
|
4. The username has the correct format, but access to the database fails.
|
||
|
|
||
|
|
||
|
#### Determine meaningful testscenarios
|
||
|
|
||
|
When it comes to creating your testscenario, we start with the easiest one: Test the wrongly formatted username. This
|
||
|
should be pretty straightforward, as we never reach the code where we need the database:
|
||
|
|
||
|
function testWrongFormat()
|
||
|
{
|
||
|
assertFalse(isValidName("$$% 1_', "Assert a name with special characters to be considered an invalid name");
|
||
|
}
|
||
|
|
||
|
The first and third scenario are more sophisticated (like always, when a database comes into the game). There are two ways
|
||
|
to test this:
|
||
|
- Either you create an empty table on each test, fill them with the users and run the test
|
||
|
- or you Mock the database call with a class that behaves like querying the users and returns true or false in each case
|
||
|
|
||
|
You **shouldn't** create a static database for all tests and assume this one to be existing - these are decoupled from the
|
||
|
actual test and soon get outdated or difficult to reflect all scenarios. Also it's not considered good practice to create
|
||
|
a precondition your tests have to rely on. In this case the second approach makes sense, as the mock class should be rather simple:
|
||
|
|
||
|
|
||
|
function testCorrectUserName()
|
||
|
{
|
||
|
// set no users in the database mock
|
||
|
$db = new UserDatabaseMock(array());
|
||
|
setupUserDatabaseMock($db);
|
||
|
|
||
|
assertTrue(isValidName("hans"), "Assert a correctly formatted and unique name to be considered valid");
|
||
|
}
|
||
|
|
||
|
function testNonUniqueUserName()
|
||
|
{
|
||
|
// set no users in the database mock
|
||
|
$db = new UserDatabaseMock(array("pete"));
|
||
|
setupUserDatabaseMock($db);
|
||
|
|
||
|
assertFalse(isValidName("pete"), "Assert a correctly formatted, but existing name to be considered invalid");
|
||
|
}
|
||
|
|
||
|
The exception can be tested by providing invalid db credentials when using a real databse or by extending the mock (which
|
||
|
we will do here):
|
||
|
|
||
|
function testDatabaseError()
|
||
|
{
|
||
|
// set no users in the database mock
|
||
|
$db = new UserDatabaseMock(array());
|
||
|
$db->setThrowsException(true);
|
||
|
setupUserDatabaseMock($db);
|
||
|
|
||
|
assertFalse(isValidName("hans"), "Assert a correct, unique user to be considered invalid when the database doesn't work");
|
||
|
}
|
||
|
|
||
|
This, of course, depends on how you want the behaviour to be when the db is down: Do you want to log the error and proceed
|
||
|
as usual or do you want the error to bubble up via an exception.
|
||
|
|
||
|
#### Writing sensible assertions
|
||
|
|
||
|
It's crucial to write sensible assertions in your test-classes: You can write a perfect test that covers the right scenario,
|
||
|
but don't catch errors because you aren't asking the correct questions.
|
||
|
|
||
|
- Write assertions that cover your scenario - if you test a correct behaviour don't test what happens if something goes wrong
|
||
|
(this is a seperate scenario)
|
||
|
- While you should try to write redundant assertions it's better to assert more than to have a missing assertion
|
||
|
- A failed assertion means that the implementation is incorrect, other assertions are to be avoided (like testing if a
|
||
|
precondition applies)
|
||
|
- When testing one function, you have to be naive and assume that everything else is bug free, testing whether other parts
|
||
|
worked correctly before testing should be made in a different test.
|
||
|
|
||
|
## How to test
|
||
|
|
||
|
Unit tests should only test an isolated, atomic of your code - in theory just one single function - and shouldn't
|
||
|
need much dependency handling. An example for a unittest would be to test the following (hypothetical) class method:
|
||
|
|
||
|
class UserManager
|
||
|
{
|
||
|
/**
|
||
|
* returns true when a user with this name exists and the
|
||
|
* password for this user is correct
|
||
|
**/
|
||
|
public function isCorrectPassword($name, $password)
|
||
|
{
|
||
|
// You needn't to know the implementation.
|
||
|
}
|
||
|
}
|
||
|
|
||
|
### The wrong way
|
||
|
|
||
|
A unit test for this user could, but should not look like this (we'll explain why):
|
||
|
|
||
|
require_once "../../library/Icinga/MyLibrary/UserManager.php";
|
||
|
// needed by UserManager
|
||
|
require_once "../../library/Icinga/Authentication/Manager.php"
|
||
|
require_once "../../library/Icinga/Authentication/User.php"
|
||
|
// .. imagine a few more require_once
|
||
|
|
||
|
use Icinga/MyLibrary/UserManager
|
||
|
|
||
|
class UserManagerTest extends \PHPUnit_Framework_TestCase
|
||
|
{
|
||
|
/**
|
||
|
* Test whether an user is correctly recognized by the UserManager
|
||
|
*
|
||
|
**/
|
||
|
public function testUserManager()
|
||
|
{
|
||
|
// Connect to the test database that contains jdoe and jsmith
|
||
|
$mgrConfg = new \Zend_Config(array(
|
||
|
"backend" => "db",
|
||
|
"user" => "dbuser.."
|
||
|
"pass" =>
|
||
|
// all the other db credentials
|
||
|
));
|
||
|
|
||
|
$mgr = new UserManager($mgrConfig);
|
||
|
|
||
|
$this->assertTrue($mgr->isCorrectPassword("jdoe", "validpassword"));
|
||
|
$this->assertTrue($mgr->isCorrectPassword("jsmith", "validpassword"));
|
||
|
$this->assertTrue($mgr->isCorrectPassword("jdoe", "nonvalidpassword"));
|
||
|
$this->assertTrue($mgr->isCorrectPassword("jsmith", "nonvalidpassword"));
|
||
|
$this->assertTrue($mgr->isCorrectPassword("hans", "validpasswor"));
|
||
|
}
|
||
|
|
||
|
This test has obviously a few issues:
|
||
|
|
||
|
- First, it assert a precondition to apply : A database must exist with the users jdoe and jsmith and the credentials
|
||
|
must match the ones provided in the test
|
||
|
- There are a lot of dependencies in this code, almost the complete Authentication code must exists. Our test
|
||
|
will fail if this code changes or contains bugs, even the isCorrectPassword method is correct.
|
||
|
|
||
|
### Reducing dependencies
|
||
|
|
||
|
To avoid these issues, you need to code your classes with testing in mind. Maybe now you're screaming *"Tests shouldn't
|
||
|
affect my code, just test if it is correct!"*, but it's not that easy. Testability should be considered as a quality aspect
|
||
|
of your code, just like commenting, keeping functions coherent, etc. Non-testable code should be considered as a bug, or
|
||
|
at least as a design-flaw.
|
||
|
|
||
|
One big buzzword in development is now Inversion of Control and Dependency Injection. You can google the details, but in
|
||
|
our case it basically means: Instead of your class (in this case UserManager) setting up it's dependencies (creating an Authentication Manager and
|
||
|
then fetching users from it), the dependencies are given to the class. This has the advantage that you can mock the
|
||
|
dependencies in your testclasses which heavily reduces test-complexity. On the downside this can lead to more complicate
|
||
|
Api's, as you have to know the dependencies of your Object when creating it. Therefore we often allow to provide an
|
||
|
dependency from the outside (when testing), but normally create the dependencies when nothing is provided (normal use).
|
||
|
|
||
|
In our case we could say that we allow our UserManager to use a provided set of Users instead of fetching it from the
|
||
|
Authmanger:
|
||
|
|
||
|
class UserManager
|
||
|
{
|
||
|
|
||
|
public function __construct($config, $authMgr = null)
|
||
|
{
|
||
|
if ($authMgr == null) {
|
||
|
// no Authmanager provided, resolve dependency by yourself
|
||
|
$this->authMgr = new AuthManager($config);
|
||
|
} else {
|
||
|
$this->authMgr = $authMgr;
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
|
||
|
It would of course be best to create an Interface like UserSource which the AuthManger implements, but in this example
|
||
|
we trust our Programmer to provide a suitable object. We now can eliminate all the AuthManager dependencies by mocking the
|
||
|
AuthManager (lets dumb it down to just providing an array of users):
|
||
|
|
||
|
require_once "../../library/Icinga/MyLibrary/UserManager.php";
|
||
|
use Icinga/MyLibrary/UserManager
|
||
|
|
||
|
class AuthManagerMock
|
||
|
{
|
||
|
public $users;
|
||
|
|
||
|
/**
|
||
|
* Create a new mock classw with the provided users and their credentials
|
||
|
*
|
||
|
* @param array $userPasswordCombinations The users and password combinations to use
|
||
|
**/
|
||
|
public function __construct(array $userPasswordCombinations)
|
||
|
{
|
||
|
$this->users = $userPasswordCombinations;
|
||
|
}
|
||
|
|
||
|
public function getUsers()
|
||
|
{
|
||
|
return $this->users;
|
||
|
}
|
||
|
}
|
||
|
|
||
|
class UserManagerTest extends \PHPUnit_Framework_TestCase
|
||
|
{
|
||
|
/**
|
||
|
* Test whether an user is correctly recognized by the UserManager
|
||
|
*
|
||
|
**/
|
||
|
public function testUserManager()
|
||
|
{
|
||
|
$authMock = new AuthManagerMock(array(
|
||
|
"jdoe" => "validpassword",
|
||
|
"jsmith" => "validpassword"
|
||
|
));
|
||
|
$mgrConfg = new \Zend_Config(array(), $authMock);
|
||
|
$mgr = new UserManager($mgrConfig);
|
||
|
|
||
|
$this->assertTrue($mgr->isCorrectPassword("jdoe", "validpassword"));
|
||
|
$this->assertTrue($mgr->isCorrectPassword("jsmith", "validpassword"));
|
||
|
$this->assertFalse($mgr->isCorrectPassword("jdoe", "nonvalidpassword"));
|
||
|
$this->assertFalse($mgr->isCorrectPassword("jsmith", "nonvalidpassword"));
|
||
|
$this->assertFalse($mgr->isCorrectPassword("hans", "validpassword"));
|
||
|
}
|
||
|
|
||
|
Ok, we might have more code here than before, but our test is now less like prone to fail:
|
||
|
|
||
|
- Our test doesn't assume any preconditions to apply, like having a db server with correct users
|
||
|
- The require call to the AuthManager is gone, so if there's a bug in the AuthManager implementation our test is not affected
|
||
|
|
||
|
|
||
|
|
||
|
### Splitting up assertions
|
||
|
|
||
|
The test is now not that bad, but still has a few issues:
|
||
|
|
||
|
- If an assert fails, we don't know which one, as the message will be rather generic ("failed asserting that False is True")
|
||
|
- In this case it might be obvious what we test, but if someone sees the class and the assertions, he doesn't know what
|
||
|
assumptions are made
|
||
|
|
||
|
To fix those issues, we have to split up our big test method in several smaller one and give the testmethod **talking names**.
|
||
|
Also, the assertions should get an error message that will be printed on failure.
|
||
|
|
||
|
/**
|
||
|
* Testcases for the UserManager class
|
||
|
*
|
||
|
**/
|
||
|
class UserManagerTest extends \PHPUnit_Framework_TestCase
|
||
|
{
|
||
|
/**
|
||
|
* Creates a new UserManager with a mocked AuthManage
|
||
|
*
|
||
|
* @returns UserManager
|
||
|
**/
|
||
|
public function getUserManager()
|
||
|
{
|
||
|
$authMock = new AuthManagerMock(array(
|
||
|
"jdoe" => "validpassword",
|
||
|
"jsmith" => "validpassword"
|
||
|
));
|
||
|
$mgrConfg = new \Zend_Config(array(), $authMock);
|
||
|
return new UserManager($mgrConfig);
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Tests whether correct user/name combinations are considered valid
|
||
|
*
|
||
|
**/
|
||
|
public function testCorrectUserPasswordCombinations()
|
||
|
{
|
||
|
$mgr = $this->getUserManager();
|
||
|
$this->assertTrue(
|
||
|
$mgr->isCorrectPassword("jdoe", "validpassword"),
|
||
|
"Asserted that a correct user/password combination is considered valid for jdoe"
|
||
|
);
|
||
|
$this->assertTrue(
|
||
|
$mgr->isCorrectPassword("jsmith", "validpassword"),
|
||
|
"Asserted that a correct user/password combination is considered valid for jsmith"
|
||
|
);
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Tests whether invalid names are rejected
|
||
|
*
|
||
|
**/
|
||
|
public function testInvalidUsernameRecognition()
|
||
|
{
|
||
|
$mgr = $this->getUserManager();
|
||
|
$this->assertFalse(
|
||
|
$mgr->isCorrectPassword("hans", "validpassword"),
|
||
|
"Asserted a non-existing user to be be considered invalid"
|
||
|
);
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Tests whether invalid passwords for existing users are rejected
|
||
|
*
|
||
|
**/
|
||
|
public function testInvalidPasswordRecognition()
|
||
|
{
|
||
|
$mgr = $this->getUserManager();
|
||
|
$this->assertFalse(
|
||
|
$mgr->isCorrectPassword("jsmith", "nonvalidpassword"),
|
||
|
"Asserted that an invalid password for an existing user is considered invalid"
|
||
|
);
|
||
|
}
|
||
|
}
|
||
|
|
||
|
Now if something fails, we now see what has been tested via the testmethod and what caused the test to fail in the
|
||
|
assertion error message. You could also leave the comments and everybody knows what you are doing.
|
||
|
|
||
|
## Testing PHP
|
||
|
|
||
|
|
||
|
## Requirements and the dependency mess
|
||
|
|
||
|
### spl_autoload_register vs. require
|
||
|
|
||
|
When looking at our test classes, you'll notice that we don't use PHPs autoloader to automatically load dependency, but
|
||
|
write 'require_once' by ourselfs. This has the following reasons:
|
||
|
|
||
|
- When writing tests, you to be aware of every dependency your testclass includes. With autoloading, it's not directly
|
||
|
obvious which classes are included during runtime.
|
||
|
- When mocking classes, you don't need to tell your autoloader to use this class instead of the one used in production
|
||
|
- Tests can't be run isolated without an boostrap class initializing the autoloader
|
||
|
|
||
|
### How to avoid require_once massacres: LibraryLoader
|
||
|
|
||
|
The downside of this approach is obvious: Especially when writing compoment tests you end up writing a lot of 'require'
|
||
|
classes. In the worst case, the PHP require_once method doesn't recognize a path to be already included and ends up
|
||
|
with an 'Cannot redeclare class XY' issue.
|
||
|
|
||
|
To avoid this, you should implement a LibraryLoader class for your component that handles the require_once calls.
|
||
|
For example, the status.dat component tests has a TestLoader class that includes all dependencies of the component:
|
||
|
|
||
|
|
||
|
namespace Tests\Icinga\Protocol\Statusdat;
|
||
|
use Test\Icinga\LibraryLoader;
|
||
|
|
||
|
require_once('library/Icinga/LibraryLoader.php');
|
||
|
|
||
|
/**
|
||
|
* Load all required libraries to use the statusdat
|
||
|
* component in integration tests
|
||
|
*
|
||
|
**/
|
||
|
class StatusdatTestLoader extends LibraryLoader
|
||
|
{
|
||
|
/**
|
||
|
* @see LibraryLoader::requireLibrary
|
||
|
*
|
||
|
**/
|
||
|
public static function requireLibrary()
|
||
|
{
|
||
|
// include Zend requirements
|
||
|
require_once 'Zend/Config.php';
|
||
|
require_once 'Zend/Cache.php';
|
||
|
require_once 'Zend/Log.php';
|
||
|
|
||
|
// retrieve the path to the icinga library
|
||
|
$libPath = self::getLibraryPath();
|
||
|
|
||
|
// require library dependencies
|
||
|
require_once($libPath."/Data/AbstractQuery.php");
|
||
|
require_once($libPath."/Application/Logger.php");
|
||
|
require_once($libPath."/Data/DatasourceInterface.php");
|
||
|
|
||
|
// shorthand for the folder where the statusdat component can be found
|
||
|
$statusdat = realpath($libPath."/Protocol/Statusdat/");
|
||
|
require_once($statusdat."/View/AccessorStrategy.php");
|
||
|
// ... a few more requires ...
|
||
|
require_once($statusdat."/Query/Group.php");
|
||
|
}
|
||
|
}
|
||
|
|
||
|
Now an component test (like tests/php/library/Icinga/Protocol/Statusdat/ReaderTest.php) can avoid the require calls and
|
||
|
just use the requireLibrary method:
|
||
|
|
||
|
use Icinga\Protocol\Statusdat\Reader as Reader;
|
||
|
|
||
|
// Load library at once
|
||
|
require_once("StatusdatTestLoader.php");
|
||
|
StatusdatTestLoader::requireLibrary();
|
||
|
|
||
|
**Note**: This should be used for component tests, where you want to test the combination of your classes. When testing
|
||
|
a single execution unit, like a method, it's often better to explicitly write your dependencies.
|
||
|
|
||
|
If you compare the first approach with the last one you will notice that, even if we produced more code in the end, our
|
||
|
test is more verbose in what it is doing. When someone is updating your test, he should easily see what tests are existing
|
||
|
and what scenarios are missing.
|
||
|
|