Skip to content
Eric Lee edited this page Apr 24, 2015 · 7 revisions

CORE uses PHPUnit for testing. Installation instructions for PHPUnit can be found here. Alternately, CORE includes PHUnit as a composer dependency. Running "composer install" will install phpunit in the vendor/bin directory.

Tests are located in the pos/is4c-nf/unit-tests/ and fannie/unit-tests/ directory. Both directories include a bootstrap.php file that should be used with phpunit's --bootstrap option. The internal functionality is different for the different sets of tests, but each is responsible for setting up class autoloading and configuration variables.

Examples of running tests:

phpunit --bootstrap pos/is4c-nf/unit-tests/bootstrap.php pos/is4c-nf/unit-tests/SearchTest.php
phpunit --bootstrap fannie/unit-tests/bootstrap.php fannie/unit-tests/ItemsTest.php

Notable Tests

Both Lane and Office include tests named InstallTest.php. These tests will create database structure and load sample data into these structures. Running this test first is often desirable because once it succeeds subsequent tests can issue test queries against the sample data. Office's InstallTest.php always uses databases named "unit_test_op" and "unit_test_trans". POS' InstallTest.php uses the currently configured database (patches welcome!). This means sample data will overwrite whatever's currently in those tables. Generally this is not a big deal since the lane database is just a copy of the authoritative back end database, but fair warning.

Both Lane and Office include tests named PluginsTest.php. Because of the way plugin detection works in both components, .php files in the respective plugin directories may be included at any time. These tests try to verify nothing catastrophic happens when any plugin file is included. Office's version is currently more comprehensive. These tests are often useful in debugging weird behavior or random crashes.

Comprehensive Example

A bug cropped up here when some bozo (@gohanman) forgot to initialize a database connection object. This results in a fatal error that crashes the whole page. This is bad. ItemModule::saveFormData() should be tested to make sure it doesn't blow up - and making sure it actually saves too isn't a terrible idea. The catch is this method relies on global state: a database record and submitted form values. To make it testable, we need to control these external pieces. One potentially helpful pattern is dependency injection.

Starting with how saveFormData() is normally called:

$form = new \COREPOS\common\mvc\FormValueContainer();
foreach ($FANNIE_PRODUCT_MODULES as $class => $params) {
    $mod = new $class();
    /** start dependency injection **/
    $mod->setConnection($this->connection);
    $mod->setConfig($this->config);
    $mod->setForm($form);
    /** end dependency injection **/
    $mod->saveFormData($upc);
}

All the information saveFormData() needs is injected into the object. When it runs, saveFormData() only needs to reference its own object. Crucially, we can inject specific values before running saveFormData() and then examine the result afterwards.

Now calling the same saveFormData() method from a unit test:

public function testItemFlags()
{
    $config = FannieConfig::factory();
    $connection = FannieDB::get($config->OP_DB);

    /**
    Setup/verify preconditions for the test
    */
    $upc = BarcodeLib::padUPC('16');
    $product = new ProductsModel($connection);
    $product->upc($upc);
    $product->load();
    if ($product->numflag() != 0) {
        $product->numflag(0);
    }
    $product->save();

    /**
    Simulate form input
    */
    $form = new \COREPOS\common\mvc\ValueContainer();
    $form->flags = array(1, 3); // 0b101 == 5

    $module = new ItemFlagsModule();
    $module->setConnection($connection);
    $module->setConfig($config);
    $module->setForm($form);

    $saved = $module->saveFormData($upc);
    $this->assertEquals(true, $saved, 'Saving item flags failed');

    $product->reset();
    $product->upc($upc);
    $product->load();
    $this->assertEquals(5, $product->numflag(), 'Wrong numflag value ' . $product->numflag());

    $product->numflag(0);
    $product->save();
}

The test chooses a product record with a well-known numflag value, feeds simulated form data into the saveFormData() method, then checks the product record to verify it was updated. There's no need to specifically check for a fatal error; that will immediately & automatically cause the test to fail. Note there's still a real database connection. Some would probably contend that a completely decoupled unit test should use a mock database object that just simulates query results. I think CORE is so database-heavy that issuing actual queries against actual databases makes for more robust tests. The InstallTests (see above) are specifically designed to build a testing database and load it with predictable sample data.

ItemFlagsModule.php itself has to be refactored a bit to actually use the injected objects:

    public function saveFormData($upc)
    {
        $flags = $this->form->flags;
        if (!is_array($flags)) {
            return false;
        }
        $numflag = 0;   
        foreach ($flags as $f) {
            if ($f != (int)$f) {
                continue;
            }
            $numflag = $numflag | (1 << ($f-1));
        }
        $dbc = $this->connection;
        $model = new ProductsModel($dbc);
        $model->upc($upc);
        $model->numflag($numflag);
        $saved = $model->save();

        return $saved ? true : false;
    }

And finally ItemModule needs the appropriate injection members and methods.

    protected $config;
    protected $connection;
    protected $form;

    public function setConfig(\FannieConfig $c)
    {
        $this->config = $c; 
    }

    public function setForm(\COREPOS\common\mvc\ValueContainer $f)
    {
        $this->form = $f;
    }

    public function setConnection(\SQLManager $s)
    {
        $this->connection = $s;
    }

This concludes the example.

Clone this wiki locally