What unit tests are
Unit tests are pieces of code that check the functionality of your backend code. They test your API by attempting to use it the way a frontend programmer would. As opposed to a frontend programmer, your tests can cover edge cases such as race conditions, security testing (e.g. against SQL injections), or buffer overflow testing, to name a few. They can also be run automatically and fast, and can assure you that modifications made to your code are not affecting the existing API or buggy.Behavior-driven development
It is often useful to write your tests before you write your implementation. That way, you can develop a clear API that both the frontend and backend programmers can agree upon. Finally, your implementation can be considered complete when it covers all testcases. There's much more than just that in the behavior-driven development pattern. You don't have to use it with Rabbit, but Rabbit was developed by teams who do, and was developed to assist teams to do it. Find out more about behavior-driven and test-driven development on google.Creating test files
You can create tests for your libraries by creating one .php file for each of your library .php files in the 'tests' directory of your application root. Like libraries, files in the 'tests' directory can be placed directly within it, or in subfolders of any depth. Although it's usually a good practice to have one unit test file for each library file, it's not necessary to do so. You can have one test file test multiple libraries, or one test file test just a part of a library.Extending Testcase
In your test file, you must create a class extending the base class Testcase. The new class name must begin with the word "Test" and be followed by a descriptive name depending on what it's testing. Then your test file must return an instance of that class. The class is used to define your tests. You must define the protected mAppliesTo variable to point to the source code location that is being tested as a string. Do not include the .php extension, and make it relative to your application root. For example, if you're testing the file 'libs/user.php', mAppliesTo will take the value 'libs/user'. The simplest test file (which does nothing at all) would look like this:<?php final class TestUser extends Testcase { protected $mAppliesTo = 'libs/user'; } return New TestUser(); ?>
Creating tests
Your testcase class is split into several test methods each of which tests for a specific functionality of your target library. This functionality should be kept at basic levels; a test method should not check for two or more different things. For example, if your system allows user registration, a test method could attempt to test user creation. You can have as many test methods as you wish.Each test method name must start with the word "Test" and be followed by a descriptive name depending on what it's testing. Test methods must be defined as public. Our class above with two empty test methods could be written as:
<?php final class TestUser extends Testcase { protected $mAppliesTo = 'libs/user'; public function TestCreate() { // ... } public function TestDelete() { // ... } } return New TestUser(); ?>
When a test if performed, all test methods are called sequentially, in the order they are defined. Each test method can be sucessful or unsuccessful. When your code is complete and not buggy, and all tests are complete and corret, all test methods will be successful. When all test methods are successful the testcase is considered successful. The status marks for success and failure are often referred to by "SUCCESS" and "FAIL".
Apart from the test class, you can also define other classes and functions to use in your tests. Be careful to make sure that everything you define is uniquely named, however. Also it is preferred to avoid any actual code execution at this point, as it will be executed even when the tests are not run. To run code prior to executing any tests, see the usage of the SetUp() method below.
Asserting
Each test method can test the behavior of the target API by performing individual assertions. Assertions are primary checks of truth or falsehood. After performing an operation, assertions can be used to validate its result. Assertions can be checked by calling the several different assertion functions of the parent class. The methods are described fully below. If an all assertions are successful, the test method is considered successful. If all test methods are successful, the testcase is considered successful. If one assertion fails, the whole test method is considered to fail. If one test method fails, the whole testcase is considered to fail.One assertion failure doesn't mean that the rest of the tests will not be executed; execution continues as normal, within the same test method. This is something to keep in mind when writing test methods that contain assertions which further assertions depend upon. See the next section on how to handle this.
Using assertions, we can fill in the test methods of the class we illustrated above:
<?php final class TestUser extends Testcase { protected $mAppliesTo = 'libs/user'; private $mUserId; public function TestCreate() { $this->Assert( class_exists( 'User' ), 'Class User doesn\'t exist' ); $user = New User(); $this->Assert( is_object( $user ), 'Could not instantiate class User' ); $this->Assert( $user instanceof User, 'Could not instantiate class User' ); $user->Name = 'Bob'; $this->AssertEquals( 'Bob', $user->Name, 'Could not set User name' ); $this->AssertFalse( $user->Exists(), 'User should not exist prior to creating them' ); $user->Save(); $this->AssertEquals( 'Bob', $user->Name, 'User did not retain its attribute values after saving' ); $this->AssertTrue( $user->Exists(), 'User must exist after they are created' ); $this->Assert( $user->Id > 0, 'User Id must be set to a positive integer, as it is autoincrement' ); $this->mUserId = $user->Id; } public function TestDelete() { $user = New User( $this->mUserId ); $this->AssertTrue( $user->Exists(), 'Failed to lookup user based on UserId' ); $user->Delete(); $this->AssertFalse( $user->Exists(), 'User must not exist after deletion' ); $user = New User( $this->mUserId ); $this->AssertFalse( $user->Exists(), 'Was able to locate user object after deleting them' ); } } return New TestUser(); ?>
The assertion methods are the following:
Assert( $value, $description )
Asserts that $value is truthy. This means that $value must be anything that causes an 'if' statement to jump into the execution code block (i.e. $value is boolean true when cast to a boolean). For example, "true", or 1.$description is a short textual message describing what the assertion is doing. All assertion methods have a $description parameter that is optional.
AssertNull( $value, $description )
Asserts that $value is null i.e. satisfies is_null().AssertNotNull( $value, $description )
Asserts that $value is not null i.e. does not satisfy is_null().AssertEquals( $expected, $actual, $description )
Asserts that the value of $actual is equal to the value of $expected. $expected should generally be your expected value, for example a hard-coded string or some other known entity, while $actual must be the value-to-check. Other assertions also offer this naming convention, and the expected value is usually the first argument of your assertion function. This method executes a value-and-type equality check, meaning that integer 1 and boolean true are not equal, only boolean true and boolean true.Be careful when comparing objects using this method; sometimes PHP can be slow or even irresponsive when comparing large objects, especially when they contain circular references or simply too many references. Prefer to compare each object's unique identifier, if there is some.
AssertNotEquals( $expected, $actual, $description )
Asserts that the value of $actual is not equal to the value of $expected. This is not the opposide of AssertEquals, as it performs a value-only test. Hence, 0 and false are not considered unequal by this function, and so it would fail with them. On the other hand, false and true are considered unequal, although they are of the same type, while 1 and false are also considered unequal and of different type.AssertTrue( $value, $description )
Asserts that $value is boolean true.AssertFalse( $value, $description )
Asserts that $value is boolean false. Notice that this is not the opposide of AssertTrue. For example, the value 1 is not boolean true, nor boolean false.Initializing and Finalizing
It's sometimes useful to perform certain operations before and after running test methods. These operations can be defined in the special functions SetUp() and TearDown(). If these are defined in your testcase, they will be called prior and after running test methods respectively. If they are defined, they must be declared with public visibility. These methods can do things such as create and remove a temporary database schema to-be-used in tests (see the relevant section Database), or initialize objects that are used in the tests.Notice that no test assertions can be performed within these functions.
A word on exceptions
Exceptions thrown within test methods (that are not handled) will terminate the execution of the current test method and all subsequent test methods. However, the TearDown() method will still be called if present, to allow you to clean up the mess. If you're running more testcases at the same time, the rest of your testcases will continue to run normally.When an exception is thrown and not handled, the test method is considered to have performed a special type of failure "UNANTICIPATED FAIL". This failure is a irrevokable failure that the testcase programmer didn't expect. You should try to limit these failures if you can, by using a try/catch pair for the exceptions you expect. It's often useful to then perform a purposely failed assertions to indicate the expected failure:
<?php public function TestDelete() { try { $user = New User( $this->mUserId ); $this->AssertTrue( $user->Exists(), 'Failed to lookup user based on UserId' ); $user->Delete(); $this->AssertFalse( $user->Exists(), 'User must not exist after deletion' ); $user = New User( $this->mUserId ); $this->AssertFalse( $user->Exists(), 'Was able to locate user object after deleting them' ); } catch ( SatoriException $e ) { // if we're doing something bad with Satori, it'll inform us accordingly // force an assertion failure $this->Assert( false, 'Something went wrong with Satori initialization: ' . $e->getMessage() ); } } ?>
Of course, it's always possible that a legitimate exception will be thrown. In that case, you can capture those and in fact require that an exception is throw. Here's an example:
<?php public function TestBadUsername() { $user = New User( $this->mUserId ); try { $user->Name = '---invalid-name'; $caught = false; } catch ( UserException $e ) { $caught = true; } $this->AssertTrue( $caught, 'Usernames cannot start with the dash character and an exception must be thrown if they do!' ); } ?>
Requiring success
It's an often case that one assertion will be a required condition for the following to complete. For instance, one could check that the 'User' class exists prior to instantiating it and testing its usage. It's possible to mark an assertion as required to continue using the RequireSuccess() method, passing it the return value of the assertion function. If the assertion fails, the current test method is aborted along with all subsequent test methods. TearDown(), however, is called nevertheless at the end, if present:<?php public function TestCreate() { $this->RequireSuccess( $this->Assert( class_exists( 'User' ), 'Class User doesn\'t exist' ) ); // we can't continue if the 'User' class is not defined! $user = New User(); $this->Assert( is_object( $user ), 'Could not instantiate class User' ); $this->Assert( $user instanceof User, 'Could not instantiate class User' ); $user->Name = 'Bob'; $this->AssertEquals( 'Bob', $user->Name, 'Could not set User name' ); $this->AssertFalse( $user->Exists(), 'User should not exist prior to creating them' ); $user->Save(); $this->AssertEquals( 'Bob', $user->Name, 'User did not retain its attribute values after saving' ); $this->AssertTrue( $user->Exists(), 'User must exist after they are created' ); $this->Assert( $user->Id > 0, 'User Id must be set to a positive integer, as it is autoincrement' ); $this->mUserId = $user->Id; } ?>
Running tests
Rabbit provides an HTML interface for running unit tests when you're in development environment. Simply point your browser to your application root directory, and navigate to the 'unittest' page. You can change the name of the page if you want by changing its route in the "project" library.The testing interface will allow you to select which tests to run and inform you of the success and failure of individual testcases, test methods, and assertions. If you desire to use a different frontend instead of HTML, for instance CLI or LaTeX, you can easily develop your own frontend for the unit testing system. Simply look at the short frontend elements at elements/developer/test.