Writing Tests

PHPUnit tests

PHPUnit tests are related to a unit you are testing. The path of the tests is identical to the path of the unit inside the tests/…​ folder. Each Unit test class has a suffix …​Test and should extend the SuiteCRM\Test\SuitePHPUnitFrameworkTestCase class. Test methods prefix is test…​.

E.g. if the /path/to/Example.php contains a class Example then the related test should be in the tests/path/to/ExampleTest.php:

Example for a PHPUnit test

contents of tests/path/to/ExampleTest.php:

use SuiteCRM\Test\SuitePHPUnitFrameworkTestCase;

// note: `SuitePHPUnitFrameworkTestCase` extends `SuiteCRM\TestCaseAbstract`, which extends `PHPUnit_Framework_TestCase`.

class ExampleTest extends SuitePHPUnitFrameworkTestCase {

    public function testDoingSomething() {
        $example = new Example();
        $results = $example->doSomething();
        $this->assertSame('expected value', $results);
    }

}

See more about PHPUnit tests at https://phpunit.readthedocs.io


Test classes:

  • abstract class SuiteCRM\TestCaseAbstract

Implementation of an abstract test case, this class overrides the setUp() and tearDown() methods to handle the database transaction creation and rollback.

State-safe Tests

Formerly, we had StateSaver and StateChecker helpers to make it easier to write state-safe tests. This proved slow and complex, so we’ve replaced it with a more standard system involving database transactions that are rolled back following each individual unit test.

Keep the system global environment state clean, especially in tests.
If a test leaves some extra data in the database, file system, superglobals, etc. this could change the behaviour of the other tests run afterward. When you write a new test make sure it is state-safe, which means tests should not leave any 'garbage' data in the test environment’s state.

In order to ensure state remains unchanged when a unit test is run, there are a few things you should keep in mind:

  • Database state, this is handled automatically by the database transaction rollback system. However, if you explicitly create a database transaction in your test you’ll need to make sure the test uses the truncate cleanup strategy instead of the default transaction strategy.

  • Changes to files on the filesystem, currently we don’t handle this.

  • Error level, if your test relies on a specific error level, you should save the error reporting level that exists at the start of the test, change the error level, then restore it at the end of the test.

  • Global PHP variables, these can be handled by PHPUnit itself, though we don’t currently use that functionality.

  • PHP INI changes, these need to be handled manually in the same way as error levels.

An example of error reporting level restoration in a test:

<?php

use SuiteCRM\Test\SuitePHPUnitFrameworkTestCase;

class somethingTest extends SuitePHPUnitFrameworkTestCase {
    public function testSomething() {
        // Store the current error level and then set the reporting level to 0 for the test.
        $errorLevelStored = error_reporting();
        error_reporting(0);

        // Do test stuff

        // Restore error reporting level.
        error_reporting($errorLevelStored);
    }
}

How to: Testing Imap

Developers able to write IMAP test (unit and acceptance tests).

If developer mode set and logger level is debug the ImapHandler class logs each imap calls, then each imap method call logged with called parameter and return values so that the developers and testers can see exactly whats going on in the background. If imap_test set, the system use fake calls by pre defined method parameters and return values in ImapHandlerFakeCalls.php so that the developers able to add more tests for any email functionality even if it needs a valid imap resource.

  • ImapHandler: Wrapper class for functions of IMAP PHP built in extension.

  • ImapHandlerFacotry: Retrieves an ImapHandlerInterface. It could be ImapHandler or ImapHandlerFake. Use $sugar_config['imap_test'] = true in config_override.php to set test mode on.

  • ImapHandlerFake: Wrapper class for functions of IMAP PHP built in extension. (tests only)

  • ImapHandlerFakeCalls.php: describes the fake imap functions return values for each function calls with a specific parameters in every test scenario.

  • ImapHandlerFakeData: For tests only, it deals fake return values for fake calls on an IMAP wrapper.

  • ImapHandlerInterface: IMAP wrappers need to implements so that the system can use it as an IMAP handler.

  • ImapTestSettingsEntry.php: for an entry point to set the current test scenario in any acceptance test can call it. (entry point example: index.php?entryPoint=setImapTestSettings&imap_test_settings=[index of array in ImapHandlerFakeCalls.php])

Example usage in Unit Tests:

first needs to include the following files:

include_once __DIR__ . '/../../../../../include/Imap/ImapHandlerFakeData.php';
include_once __DIR__ . '/../../../../../include/Imap/ImapHandlerFake.php';

Example unit test for imap connection (using fake imap data)

     public function testConnectMailserverUseSsl()
     {

         // using fake imap handler behaviour in test
         $fake = new ImapHandlerFakeData();

         // set up the fake handler behaviour
         $fake->add('isAvailable', null, [true]);
         $fake->add('setTimeout', [1, 60], [true]);
         $fake->add('setTimeout', [2, 60], [true]);
         $fake->add('setTimeout', [3, 60], [true]);
         $fake->add('getErrors', null, [false]);
         $fake->add('getConnection', null, [function () {
             // the current crm code needs a valid resource to an imap server
             // but also will accept a file resource
             return fopen('fakeImapResource', 'w+');
         }]);
         $fake->add('getMailboxes', ['{:/service=/notls/novalidate-cert/secure}', '*'], [[]]);
         $fake->add('ping', null, [true]);
         $fake->add('reopen', ['{:/service=}', 32768, 0], [true]);

         // instantiate a fake imap handler
         $imap = new ImapHandlerFake($fake);

         $_REQUEST['ssl'] = 1;

         // using fake imap in InboundEmail class (only for testing)
         $ie = new InboundEmail($imap);

         // test connection, it should pass
         $ret = $ie->connectMailserver();
         $this->assertEquals('true', $ret);
     }

useful config variables:

$sugar_config['imap_test'] = true;
$sugar_config['logger']['level'] = 'debug';
$sugar_config['stack_trace_errors'] = false; // set to true for more details
$sugar_config['developerMode'] = true;
$sugar_config['show_log_trace'] = false; // set to true for more details

References

Content is available under GNU Free Documentation License 1.3 or later unless otherwise noted.