Skip to content

Overview

XELOS integrates the popular testing framework Codeception to support the writing of different types of tests. Currently Unit Tests and Functional Tests are supported.

XELOS 7.1 supports tests only for the core functions - module specific tests are supported since XELOS 8.0. Core tests are being located in the /tests/ folder of the system, while module tests are saved in the respective module folder.

Namespacing

The namespacing for tests inside Modules should be as follow:

The following paths should map the corresponding namespaces:

/server/xelos/modules/lists/tests/codeception/unit/ListsControllerTest.php
namespace XELOS\Modules\Lists\Tests\Codeception\Unit\ListControllerTest

/server/xelos/modules/lists/tests/codeception/support/Helper/ListsHelper.php
namespace XELOS\Modules\Lists\Tests\Codeception\Support\Helper\ListHelper

The namespacing in the global test directory (/server/xelos/tests) is analog to the module namespaces:

/server/xelos/tests/codeception/unit/SomeTest.php
namespace XELOS\Tests\Codeception\Unit\SomeTest

/server/xelos/tests/codeception/support/Helper/SomeHelper.php
namespace XELOS\Tests\Codeception\Support\Helper\SomeHelper

To uphold codeception standards, the codeception folder itself and the next folder should be lower case, and the folders after the codeception folders should be ucfirst():

[...]/tests/codeception/unit/Helper/Some/More[...] [...]/tests/codeception/support/Lib/Api[...]

The defined namespace in the codeception.yml files has to map the the defined "settings" folder in "path":

# codeception.yml in: /server/xelos
namespace: XELOS\Tests\Codeception\Support
paths:
 support: tests/codeception/support

otherwise the enabled modules will not be correctly resolved:

# unit.suite.dist.yml in: /server/xelos/tests/codeception
namespace: XELOS\Tests\Codeception\Support
modules:
  enabled:
    - XELOS\Tests\Codeception\Support\Module\XELOSBootstrapper

# The Codeception Autoloader will look for the XELOSBootstrapper file
# in the Module/ subfolder of the previously defined support path 

Running Test

Test can be run using php xf test all. To be able to run a test you must have a working XELOS installation. Furthermore, you need a config.tests.php configuration in your /system/config/ folder. This can be a copy of your config.custom.php file but should be linked to another database as running the tests will wipe the existing database. Make sure that the user and database exists before continuing.

To run tests locally you need composer dev modules installed. Run composer update.

To run tests for specific modules you can execute the following command:

php xf test [unit|acceptance|...] [module]

E.g.:php xf test acceptance lists or php xf unit translation

During writing tests it might be helpful to run only a specific test to reduce the waiting time. This can be done by specifying the suite and test file name - e.g. php xf test unit core/xelosDatabaseTest.php or php xf test functional Core/basicFormCheckCest.php

Note: You can also run tests directly via Codeception, e.g. ./vendor/bin/codecept run functional Core/basicFormCheckCest.php. Please refer to the official Codeception docs for more information about syntax and features: https://codeception.com/docs/reference/Commands#Run

You can see the return of your test in the codeception/_output directory.

Tipps und Tricks

  1. Immer Cest-Klassen verwenden (da Cept Dateien Scripte sind, welches ein Scenario nutzen, womit man nur einen Kontext abbilden kann und eher an Nicht-Entwickler gerichtet ist)

  2. Erstellen von Funktionen in Cest-Klassen: public Funktionen werden immer als Testfunktionen ausgeführt, möchte man ein Funktion innerhalb der Klasse, als Helper und nicht als Test, muss man die Funktion entweder protected deklarieren oder wenn man sie public macht/braucht den Methodenname mit einem Unterstrich prefixen. Bsp: _myFunction.

  3. Use-Case und Codeception-Code einfach als Akzeptanztest zusammenklicken https://chrome.google.com/webstore/detail/codeception-testtools/jhaegbojocomemkcnmnpmoobbmnkijik

  4. Testdurchläufe mit verschiedenen Daten: innerhalb der Cest-Klasse eine Method erstellen: (Access Modifier siehe Punkt 2.)

    protected function myDataProvider(){
      return [
        ['start' => '2019-04-22', 'end' => '2019-04-27', 'airportId' => 1]
      ];
    }
    
    und diese per Annotation @dataProvider myDataProvider der zu nutzenden Testfunktion bekannt machen.
    /**
     * @param Endpoints $I
     * @param \Codeception\Example $myDataProvider
     *
     * @dataProvider myDataProvider
     */
    public function comparePricesFromWebpageWithEndpoint(UserGuy $I, \Codeception\Example $myDataProvider)
    {
        $I->authenticateAsApiUser();
    
        $I->sendPOST('/webservice/json/parking',[
            "method" => "list_available_products",
            "params" => [
                "startDate" => $myDataProvider['start'],
                "endDate" => $myDataProvider['end'],
                "airportId" => $myDataProvider['airportId'],
                "lang" => "de_DE"
            ],
            "id" => 1
        ]);
        $I->seeResponseCodeIs(200);
        $I->seeResponseIsJson();
    
        foreach ($this->prices as $price){
            $I->seeResponseContainsJson(['price' => $price]);
        }
    
    }
    

Writing Unit Tests

Please make yourself familiar with general CodeCeption tests by looking into existing tests or the official documentation as we are not able to cover the basics in this document: https://codeception.com/docs/07-AdvancedUsage

A basic functional test will look like this:

<?php
class basicFormCheckCest {
    // Admin can Login
    public function tryFormBasicFunction(UserGuy $I) {
        // LOGIN
        $I->amOnPage('/');
        $I->fillField('userid', 'admin');
        $I->fillField('password', 'admin');
        $I->click(_("Login"));

        $page_url = '/systemadmin/dev/form';
        $I->wantToTest("Testing page {$page_url} < for errors");
        $I->amOnPage($page_url);
        $I->seeResponseCodeIs(200);
    }
}

Injection

It is possible to inject helper classes so you do not have to call them as static

<?php
class MyUnitTest extends \Codeception\Test\Unit {

    /** @var XFHelper $XFHelper */
    protected $XFHelper;

    public function _inject(XFHelper $XFHelper) {
        $this->XFHelper = $XFHelper;
    }

    protected function _before() {
        $this->XFHelper->resetXF();
    }

}

Mocking & Reflection Examples

How to reset the XF instance via reflection

<?php
// Available in \XELOS\Tests\Codeception\Support\Helper\XFHelper->resetXF()
$XF = XF::instance();
$XFReflection = new \ReflectionObject($XF);
$InitializedProperty = $XFReflection->getProperty('initialized');
$InitializedProperty->setAccessible(true);
$InitializedProperty->setValue($XF, false);
$XF->init();

Example: Mocking the get_available_languages() function of the i18n object and replacing it in $XF

<?php
// Create the i18n mock object
$i18n = $this->make(new \XELOS\Framework\XF\Lib\i18n, [
    'get_available_languages' => function($with_captions = false, $show_only_active = true) {
        $result = ['DE_DE' => 'Deutsch', 'EN_GB' => 'English', 'ES_ES' => 'Español'];
        return ($with_captions) ? $result : array_keys($result);
    },
]);

// Inject the mocked i18n object via reflection
$lib = \XELOS\Framework\XF::instance()->lib;
// Call i18n once to initialize the private property in $lib
$lib->i18n;
$reflection = new \ReflectionObject($lib);
$I18nProperty = $reflection->getProperty('i18n');
$I18nProperty->setAccessible(true);
$I18nProperty->setValue($lib, $i18n);

Codeception Examples for module

Create Unit Test for Test:

php vendor/bin/codecept generate:test unit ListsController -c modules/lists/tests/codeception

Validate configuration:

php vendor/bin/codecept config:validate -c modules/lists/tests/codeception/

Run functional tests for a single module:

php vendor/bin/codecept run functional -c modules/lists/tests/codeception/

Actor and Roles

In the core test folder exists two Actors AdminGuy and UserGuy, you can use this both to refine in module acceptance test your use case with clearer roles

Example:

class ModeratorGuy extends UserGuy {

    public function __construct(Scenario $scenario) {
        parent::__construct($scenario);
        $XF = xf::instance();
        $XF->user->set_user_policy(6, 'forum', 'moderate',1);
    }

    public function login($user = null, $password = null) {
        parent::login('bernd','bernd');
    }

    public function enableModerate() {
        $I = $this;
        $I->click('Enable Moderate Mode');
        $I->see('Select all');
        $I->see('Move');
    }
}

use refined role in your cest:

public function moderatorTryToMovePostFromUser(ModeratorGuy $I, Step $step) {
    $I->login();

    $step->switchToStartPage($I);
    $step->switchCategory($I);
    //$step->switchToDiscussion($I);

    $I->enableModerate();

    $step->movePost($I);
}