This document is intended to record our thinking when it comes to Unit Testing in Knickers.
Overview [back to top]
- Unit Testing is good and we should do it
- Unit Testing is so good that all classes should have a corresponding test class
- CVS should execute unit testing for any committed classes and not allow check-in for anything that fails
- Unit testing must allow reporting to different output mechanisms (text, HTML, and possibly XML)
- A directory structure parallel to common should be created in knickers/testcases to make it easy to find the test case for any file
- Unit testing: http://junit.sourceforge.net/, http://www.junit.org
- A good PHP unit testing library: http://www.simpletest.org
Class Structure [back to top]
- Test - Base class for TestCases and TestSuites (so that all testable objects descend from the same root)
- Test_Case - Base class for testing, containing the functions that all test case classes should have. This should be extended for each class we're testing
- Test_Suite - Sets up a group of tests to run
- TestReport - Class that handles gathering of test results as passed to it from unit tester script. Results are displayed via OutputGenerator
- TestEvent - Basic object to track pass/fail state of a specific test method and any special messages it generates. Generated by TestCase and processed into display information by TestReport
Implementation of a Test Case [back to top]
All test case classes must extend common/cls/Test/Test_Case.class.php or extend another Test_Case class.
At the very minimum, your test case class must require_once the test case class you are extending, and the actual test you are testing.
Test cases should be placed in the testcases directory, in subdirectories parallel to those in the Knickers root directory. This will allow the UnitTester to find them automatically. (e.g., the error object's test case would be knickers/testcases/common/cls/Error/Test_Case_Error.class.php) (a custom error class for someCompany might be in knickers/testcases/custom/someCompany/cls/Error/Test_Case_Error.class.php
A test case may have a setUp and tearDown function if some set of steps must occur before and after each test method is run. Otherwise, all test methods must begin with the word "test" and be either private or protected. To test an assertion, use one of the assert() methods built into the Test_Case class. The assertion's pass/fail will be recorded internally and reported by the TestReport object. It is not necessary for you to do anything other than setup your methods and use the assert functions.
Running Tests [back to top]
To run a test for a particular class or file, from the command line, run KNICKERS_ROOT/scripts/unit_tester.php [path to your file]
The tester script will find the appropriate test case class and run all of its (and its parents') methods. If all tests pass, "OK" will be displayed. If any tests fails "FAILURE" will be displayed. All failed tests, including their class, method name, and additional descriptions will be displayed. By default, passed tests are not displayed. (Run unit_tester with no arguments to see a complete list of options.)
Example passed test: Test run for /home/cmullin/knickers/common/cls/Test/Test_Case.class.php OK Test cases run: 11, Passes: 11, Failures: 0 Example failed test: Test run for /home/cmullin/knickers/common/cls/Object.class.php FAILURE FAILED test_case_object: testChainDoNoArgs on CDMJ_ERROR returned FAILED test_case_object: testChainDoNoArgs on Illegal argument error received FAILED test_case_object: testChainDoBadSyntax on CDMJ_ERROR returned FAILED test_case_object: testChainDoBadSyntax on Illegal argument error received FAILED test_case_object: testChainDoNotObject on CDMJ_ERROR returned Test cases run: 23, Passes: 18, Failures: 5
Using Alternate Storage [back to top]
You may want to run your unit tests with different storage from what your application typically uses. There are several different methods for doing so, depending on your desired outcome. (Note: At this point in time, this primarily affects only Things and database connections.)
Temporarily point your whole application at another storage location.
Modify your storage file to point elsewhere. Original File, myapp.conn.php: $storageInfo['storage_db']['active'] = TRUE; $storageInfo['storage_db']['host'] = 'my-db-host'; $storageInfo['storage_db']['db'] = 'my_db'; $storageInfo['storage_db']['user_s'] = 'my_user'; $storageInfo['storage_db']['user_w'] = 'my_user'; $storageInfo['storage_db']['user_r'] = 'my_user'; $storageInfo['storage_db']['password_s'] = 'abc'; $storageInfo['storage_db']['password_w'] = 'def'; $storageInfo['storage_db']['password_r'] = 'ghi'; $storageInfo['storage_db']['dbtype'] = 'MySQL'; ---------------------------------------------- Modified File, myapp.conn.php: $storageInfo['storage_db']['active'] = TRUE; /* Temporarily not using this $storageInfo['storage_db']['host'] = 'my-db-host'; $storageInfo['storage_db']['db'] = 'my_db'; $storageInfo['storage_db']['user_s'] = 'my_user'; $storageInfo['storage_db']['user_w'] = 'my_user'; $storageInfo['storage_db']['user_r'] = 'my_user'; $storageInfo['storage_db']['password_s'] = 'abc'; $storageInfo['storage_db']['password_w'] = 'def'; $storageInfo['storage_db']['password_r'] = 'ghi'; $storageInfo['storage_db']['dbtype'] = 'MySQL'; */ $storageInfo['storage_db']['host'] = 'my-other-db-host'; $storageInfo['storage_db']['db'] = 'my_other_db'; $storageInfo['storage_db']['user_s'] = 'my_other_user'; $storageInfo['storage_db']['user_w'] = 'my_other_user'; $storageInfo['storage_db']['user_r'] = 'my_other_user'; $storageInfo['storage_db']['password_s'] = 'jkl'; $storageInfo['storage_db']['password_w'] = 'mno'; $storageInfo['storage_db']['password_r'] = 'pqr'; $storageInfo['storage_db']['dbtype'] = 'MySQL';
Point all methods in a Test_Case at an alternate storage file.
Point config to an alternate storage file. require_once KNICKERS_ROOT.'/common/cls/Test/Test_Case.class.php'; require_once KNICKERS_ROOT.'/common/cls/Config.class.php'; require_once KNICKERS_APP_ROOT.'/common/cls/Modeling/Thing_MyThing.class.php'; class Test_Case_Thing_MyThing extends Test_Case { /** * Setup alternate storage connection * *@access public *@return void */ function setUp() { $this->cfg =& Config::getInstance(); // tell this to load $this->cfg->loadStorageFile('test_alt.conn.php'); } /** * Test some stuff * *@access public *@return void */ function testMyThing() { $thing =& new Thing_MyThing($this->cfg); // ... } }
Point a single method in a Test_Case at alternate storage information (no file).
Point config to an alternate storage file. require_once KNICKERS_ROOT.'/common/cls/Test/Test_Case.class.php'; require_once KNICKERS_ROOT.'/common/cls/Config.class.php'; require_once KNICKERS_APP_ROOT.'/common/cls/Modeling/Thing_MyThing.class.php'; class Test_Case_Thing_MyThing extends Test_Case { /** * Test some stuff * *@access public *@return void */ function testMyThing() { $cfg =& Config::getInstance(); $storageInfo['host'] = 'my-other-db-host'; $storageInfo['db'] = 'my_other_db'; $storageInfo['user_s'] = 'my_other_user'; $storageInfo['user_w'] = 'my_other_user'; $storageInfo['user_r'] = 'my_other_user'; $storageInfo['password_s'] = 'jkl'; $storageInfo['password_w'] = 'mno'; $storageInfo['password_r'] = 'pqr'; $storageInfo['dbtype'] = 'MySQL'; $cfg->setStorageInfo($storageInfo, 'storage_db'); $thing =& new Thing_MyThing($cfg); // ... } }