Skip to content
This repository

HTTPS clone URL

Subversion checkout URL

You can clone with HTTPS or Subversion.

Download ZIP

Lab is stupid testing "framework" for PHP

branch: master

Fetching latest commit…

Octocat-spinner-32-eaf2f5

Cannot retrieve the latest commit at this time

Octocat-spinner-32 docs
Octocat-spinner-32 parody @ 78df365
Octocat-spinner-32 src
Octocat-spinner-32 tests
Octocat-spinner-32 .gitignore
Octocat-spinner-32 .gitmodules
Octocat-spinner-32 .travis.yml
Octocat-spinner-32 LICENSE.md
Octocat-spinner-32 README.md
Octocat-spinner-32 composer.json
Octocat-spinner-32 lab.config
Octocat-spinner-32 lab.php
Octocat-spinner-32 sage.config
README.md

Lab

Lab is a stupid test "framework" that works well with Parody (and includes it by default) to provide much of the same testing capacity as more complex frameworks.

Build Status

Installation

git clone --recursive https://github.com/dotink/Lab.git <path where you want to install lab>

Basic Usage

Once you have lab installed, getting started is easy!

cd <path to project>
cp <path to lab>/lab.config ./lab.config
mkdir tests

You can now run lab for the first time

Lab running for the first time

Adding a Test Set and Tests

To add a set of tests to Lab you can create a file in the tests folder with the name of the test set you want. Unlike other testing frameworks, this file is a simple PHP file that just returns an array. Let's add the following template to a file called Example Tests.php:

<?php namespace Dotink\Lab
{
    return [
        'tests' => [

            // Our first test

            'First Test' => function($data){

            },
        ]
    ];
}

We can now re-run Lab and see our first test result:

Lab running our first test

Making an Assertion

Each test will make one or more assertion. Lab provides an Assertion class which can be used to make assertions, however, you can use a third party library or basic PHP code if you prefer. Regardless of how you make assertions, a test will fail in Lab whenever an uncaught exception is thrown. Let's examine this with a basic assertion using the Lab assert function which will create a new instance of the build in Assertion class.

<?php namespace Dotink\Lab
{
    return [
        'tests' => [

            // Our first test

            'First Test' => function($data){
                assert(2 + 2)->equals(5);
            },
        ]
    ];
}

This time, when we rerunning Lab, we see the failure immediately as well as some information regarding the failure:

Lab with a failing assertion

"Smart" Assertions

The Assertion class is designed to provide features for the 90% of test cases you will need to run with a concise and flexible syntax. Part of its convenience is how it parses string input and attempts to determine whether or not the string represents another piece of code, for example, a class method, a property, or a function. The following two lines of code are, for the most part, equivalent:

assert(ltrim('test', 't'))->equals('est', TRUE);

assert('ltrim')->with('test', 't')->equals('est');
Multiple Assertions

Although each of the above will do the exact same comparison to assert equal values, the second benefits in two ways. Firstly, by providing a function, method, or property name to assert() directly, it is possible to run multiple assertions over a single method without repeating the actual method name:

assert('ltrim')
    -> with   ('test', 't')
    -> equals ('est')

    -> with   ('another', 'an')
    -> equals ('other')

    -> with   ('  default  ')
    -> equals ('default  ')
;
Testing Private/Protected

In addition to running multiple assertions easily, using a "smart" assertions allows you to access private and protected methods and properties. Let's use the following absolutely useless class to illustrate this:

class Adder
{
    protected $seed = 0;

    public function __construct($seed)
    {
        $this->seed = $seed;
    }

    private function add($num)
    {
        return $this->seed + $num;
    }
}

Using the class above we can easily do the following assertions despite that the add method is not publicly visible:

assert('Adder::add')
    -> using  (new Adder(3))
    -> with   (2)
    -> equals (5)

    -> using  (new Adder(5))
    -> with   (3)
    -> equals (8)
;

Similarly, we could check the value of the seed property with a slightly different call:

assert('Adder::$seed')
    -> using  (new Adder(3))
    -> equals (3)

    -> using  (new Adder(5))
    -> equals (5)
;

The above code also shows how we can call the using method to specify on which object we want to access the property or method. This allows us to easily test a number of objects which may have been instantiated differently to ensure that behavior is consistent across a wider number of cases.

Fixtures

It is important to understand that each test set / file which is added to Lab will run in a completely separate execution of PHP. Although we recommend organizing test sets per fixture, you can create separate fixture includes and add them across multiple test sets if need be. In all cases, anything you need to do to prepare your testing environment or create data to test against should be added to the setup key in the test set array:

<?php namespace Dotink\Lab
{
    return [
        'setup' => function($data) {
            // setup code here
        },

        'tests' => [
            // tests here
        ]
    ];
}

If you have any setup that is required across all test sets / files, then you can add that logic to the lab.config file in the closure referenced by the same key name. Similarly you will also find a cleanup key there which can be used for global cleanup code as well as added to each separate test set for specific cleanup code:

//
// The global 'cleanup' key can contain a closure to run fixture cleanup logic at the end
// of every test file
//

'cleanup' => function($data) {

},

Custom Configuration Data

By now you may have realized that every closure either in the lab.config file or in a test set takes in a $data parameter. You may have also noted the data key in the lab.config which points to an array. By default this array contains only a root key which points to the directory where the lab.config file is found.

You can add any arbitrary pieces of information you might need on a per test / setup / or cleanup basis to this array. Or use the provided root, for example, to load your classes using the needs function:

'setup' => function($data) {
    needs($data['root'] . '/src/Adder.php');
}

Dependencies

The needs function, as seen in code above, provides a clean way to require your source files with the nicely formatted output of Lab:

Lab with a failing needs

You may be, however, otherwise tempted to throw an autoloader or something similar in your setup function(s). While this is 100% possible, you'll need to set the disable_autoloading to FALSE in the lab.config file. If this is set to TRUE (default), Lab will register an autoloader almost immediately which will prevent (by throwing an Exception) classes from being loaded in that manner. This is to reduce the chance that an unknown or unseen dependency will cause your unit tests to become something more like integration tests.

Aside from a few edge cases where not suitable, Lab will look for and include the Parody submodule if it exists. Parody is a flexible mimicking library which can build dynamic runtime-configurable classes to match your static / object dependencies. Unlike traditional mock object libraries, Parody does not extend and overload classes, nor does it use reflection to try and manipulate them into meeting your expecations. Instead, Parody assumes the strict context of a framework like Lab and builds a class in the same exact namespace and with the same exact name as your dependency.

We suggest you check out Parody's github repo for more information, but for now, here's a quick example of how it could mimic our Adder class if we were statically dependent on it in another class:

Mime::define('Adder');

Mime::create('Adder')->onNew('3', function($mime) {
    $mime->onCall('add')->expect(2)->give(5);
    $mime->onGet('seed')->give(3);
});

In short, the above defines our Adder class, creates it, then specifies that when a new Adder is instantiated with an argument of 3 we should return 5 if we receive a call to add with an argument of 2, or give 3 if an attempt is made to get the seed property.

Note: It is likely not possible (we haven't tried) to make assertions on a mimicked class, at least certainly not using "smart" assertions.

Conclusion

Lab is an easy-to-use, quick-to-set-up, and generally fun testing framework. Combined with Parody it represents a powerful tool for PHP testing which encompasses a lot of best practices with regards to testing, including by not limited to:

  • Simple, clear, and expressive API (limits mistakes in tests themselves)
  • High degree of code isolation (disabling autoloading by default, explicit needs or mimicking)
  • Limiting irrelevant context (each test set is executed independently by PHP)
  • Non-negotiable hard fails (complete death on failure until it's resolved)
Something went wrong with that request. Please try again.