Unit Tests

The main task of tests is to ensure the maximum reliability of the code. The most important rule to remember is that a test should bring the system to a reference state, cover business logic, and verify that all data is correct, then return the system to the reference state. Try to make all your tests as isolated as possible from other tests.

Usually, tests are located in the tests folder at the root of the project.

Examples of how to write tests for DGS can be found in vendor/festi-team/festi-framework-core/tests.

Base Structure

/var/www/[USER_NAME]/data/www/:

[PROJECT_NAME]/
|...
|-- tests/
    |-- Plugins/
    |   |-- CompaniesPluginTest.php
    |   |-- UsersPluginTest.php
    |   ...
    |-- Resources/
    |-- Responses/
    |   |-- GitlabProjects.json
    |   ...
    | ...
    |-- bootstrap.php
    |-- phpunit.xml
    |-- [PROJECT_NAME]TestCase.php

Get Started

  1. Append to composer.json:
  "autoload-dev": {
    "psr-4": {
      "": [
        "tests",
        "vendor/festi-team/festi-framework-core/tests"
     ]
   }
 }
  1. Create bootstrap.php:

You can use external variables DB_ROOT_USER, DB_ROOT_PASSWORD to run tests from gitlab pipelines.

export DB_ROOT_USER="user_with_root_privileges"
export DB_ROOT_PASSWORD="Password123!"
vendor/bin/phpunit

tests/bootstrap.php

<?php

define('PHPUnit', true);

define('FS_ROOT', realpath(__DIR__.'/../').DIRECTORY_SEPARATOR);
define('FS_TESTS_ROOT', __DIR__.DIRECTORY_SEPARATOR);

if (!defined('MYSQL_PATH')) {
    define('MYSQL_PATH', getenv('MYSQL_PATH') ?: '/usr/bin/mysql');
}

$GLOBALS['config'] = array();

require_once 'vendor/autoload.php';

// using manual configuration for test database(usually located in tests/config.php)
$GLOBALS['config']['db']['dsn']  = $GLOBALS['DB_DSN'];
$GLOBALS['config']['db']['user'] = $GLOBALS['DB_USER'];
$GLOBALS['config']['db']['pass'] = $GLOBALS['DB_PASSWD'];    

// using auto configuration for test database(creating and dropping)
$GLOBALS['config']['db'] = (new TestDatabase(DataAccessObject::TYPE_MYSQL, MYSQL_PATH))->initialize(
 getenv('DB_ROOT_USER') ?: 'root',
 getenv('DB_ROOT_PASSWORD') ?: 'password'
);

$GLOBALS['FS_ROOT'] = FS_ROOT;

require_once FS_ROOT."common.php";

require_once __DIR__.DIRECTORY_SEPARATOR.'[PROJECT_NAME]TestCase.php';

$options = array(
  Core::OPTION_THEME_NAME      => 'default',
  Core::OPTION_ENGINE_FOLDER      => 'core',
  Core::OPTION_PLUGINS_FOLDER      => 'plugins',
  Core::OPTION_CONNECTION     => DataAccessObject::factory($db),
  'convert_path'        => '/usr/bin/convert',
);

$core = Core::getInstance($options);

$core->config = $GLOBALS['config'];

$core->user = new DefaultUser($GLOBALS['_sessionData']);

TestUtils::cleanDatabase();
TestUtils::installDatabase(FS_ROOT.'dump'.DIRECTORY_SEPARATOR);

$systemPlugin = $core->getPluginInstance('Jimbo');
$core->setSystemPlugin($systemPlugin);

$systemPlugin->onInitRequest();

If TestUtils::installDatabase throws an error, you need to declare the MYSQL_PATH constant in bootstrap.php.

define('MYSQL_PATH', '/usr/bin/mysql');
  1. Create phpunit.xml:
<?xml version="1.0" encoding="UTF-8"?>
<phpunit backupGlobals="false"
         bootstrap="./bootstrap.php"
         convertErrorsToExceptions="true"
         convertNoticesToExceptions="true"
         convertWarningsToExceptions="true"
         forceCoversAnnotation="false"
         verbose="true"
>
    <testsuites>
        <testsuite name="Plugins">
            <directory>./Plugins/</directory>
        </testsuite>
    </testsuites>

    <filter>
        <blacklist>
            <directory>../tests/</directory>
            <directory>../themes/</directory>
            <directory>../vendor/</directory>
        </blacklist>
    </filter>

    <php>
        <var name="DB_DSN" value="mysql:dbname=festi_tests;host=localhost" />
        <!-- <var name="DB_DSN" value="sqlsrv:Server=;Database=festi_tests" /> -->
        <!-- <var name="DB_DSN" value="pgsql:dbname=festi_tests;host=localhost" /> -->
        <var name="DB_USER" value="developer" />
        <var name="DB_PASSWD" value="developertest" />
        <var name="DB_DBNAME" value="festi_tests" />
    </php>

    <logging>
        <log type="junit" target="./reports/phpunit.xml" />
        <log type="coverage-clover" target="./reports/phpunit.coverage.xml" />
    </logging>

</phpunit>

Don't forget that tests should be run on another database, and you should triple-check that you don't accidentally run tests on your production database.

  1. It is usually convenient to have a parent class for tests that all tests in the project inherit from. This class is often named tests/[PROJECT_NAME]TestCase.php.
class [PROJECT_NAME]TestCase extends FestiTestCase
{
  protected function setUp()
  {
      $this->core = Core::getInstance();
  } // end setUp

  protected function tearDown()
  {
  }
}
  1. Create tests/Plugins and put all tests related to plugins there, for example: tests/Plugins/AppPluginTest.php

class AppPluginTest extends [PROJECT_NAME]TestCase
{
   public function testOnVersionGet()
   {
      $appPlugin = $this->core->getPluginInstance('App');
      $apps = $appPlugin->object->getApps();

      $this->assertNotEmpty($apps);

      foreach ($apps as $app) {
          $this->assertArrayHasKey('version', $app);
      }
   } // end testOnVersionGet
}
  1. Run Unit test:
cd tests
phpunit

You can run specific test:

phpunit Plugins/AppPluginTest.php

Mock Plugin

Often when writing tests, you need to mock a method call that, for example, loads remote data from some API.

The FestiTestCase class has a getPluginMockBuilder method that generates a MockObject for the plugin.

Example:


$appPlugin = $this->getPluginMockBuilder('App', array('getLicenseValuesObject'));

$appPlugin->method('getLicenseValuesObject')
          ->willReturn('foo');

$response = new Response();
$appApiPlugin->onDownloadGet($response);

$this->assertTrue($response->status);

Mock Action

Method getActionMockBuilder generate MockObject for action:


$actionMock = $this->getActionMockBuilder(
    'CsvImport',
    array('getUploadFilePath')
);

Unittest for plugins

Read in the plugins section

Autogenerator Unit tests for Store

To cover all actions in DGS, you can use the doTestStore method.

class  AutoCreateStoreTest extends FestiTestCase
{
    public function testCreateStoreTests()
    {
        $this->doTestStore("site_contents");
    }
}

The method generates the tested methods for the main actions: list, insert, edit, info, remove.

If an unknown action is encountered, you will need to create a method in your class onTest[ACTION_NAME]StoreAction($store, $values) that should cover this action

Working with DGS`s Child Relation

$values = array(
    'column_name' => 'test_column',
    'type' => 'string'
);

$parentValues = array(
    'id_storage' => $storageData['id']
);

static::prepareChildRelationPostByStoreName(
    "game_world_app_storage_columns",
    $storageData['id'],
    $values,
    $parentValues
);

$store = $this->plugin->yourPluginName->onDisplayStorageFields($response);

list($data) = $store->load();

$this->assertTrue($data['column_name'] == "test_column");

Theme tests

To write tests for a theme, you need to have a tests folder in the root of your theme, and you need to write tests for the theme there. We recommend inheriting tests from the ThemeTestCase class.

class HeaderThemeTest extends ThemeTestCase
{
    public function testAddContentRight(): void
    {
        $testContent = 'testAddContentRight';

        $this->core->addEventListener(
            Theme::EVENT_THEME_HEADER_CONTENT_RIGHT,
            function (FestiEvent &$event) use ($testContent) {
                return $testContent;
            }
        );

        $content = '';
        $displayPlugin = new DisplayPlugin();

        $result = $displayPlugin->fetchMain($content);

        $this->assertTrue(is_int(strpos($result, $testContent)));
    }
}

FAQ

Environment Variables

To set custom database access, you can use environment variables:

  • DB_TYPE - Type of database (mysql, mssql, pgsql). Default: mysql
  • DB_USER - Default: developer
  • DB_PASSWORD- Default: developertest
  • DB_NAME - Default: festi_tests
  • DB_HOST - Default: localhost
  • DB_PORT - Default: null- the standard port for the database type will be used.
export DB_NAME=festi_tests2
cd tests
php ../vendor/bin/phpunit 

SQL Server

CREATE DATABASE festi_tests;
CREATE LOGIN developer WITH PASSWORD = 'developertest1@';
CREATE USER developer FOR LOGIN developer;

USE festi_tests;
EXEC sp_addrolemember 'db_owner', 'developer';

Permission

If you need to test logic related to permission checks through the Permission Section, you can do it like this:

public function test_onDisplayManage()
{
    $gamesPlugin = $this->core->getPluginInstance('Games');

    $response = new Response();

    $hasException = false;
    try {
        $gamesPlugin->onDisplayManage($response);
    } catch (PermissionsException $exp) {
        $hasException = true;
    }

    $this->assertTrue($hasException);

    $this->core->getSystemPlugin()->setPermissionSection(
        'manage_games', 
        ISystemPlugin::PERMISSION_MASK_EXECUTE, 
        $this->core->user->getRole()
    );
    $this->core->getSystemPlugin()->refreshPermissionSections();

    $gamesPlugin->onDisplayManage($response);

    $this->assertNotEmpty($response->getContent());
}

Usage annotations

In our testing practices, we have made a conscious decision not to utilize annotations provided by PHPUnit. This decision is based on our desire to avoid potential compatibility issues with different versions of PHPUnit and to maintain flexibility in our testing framework.

By not relying on annotations, we ensure that our test cases remain independent of the specific version of PHPUnit being used. This allows us to update PHPUnit or switch to alternative testing frameworks in the future without impacting our existing tests.

Instead of using annotations, we structure our tests to provide data directly within the test methods. This approach allows us to have greater control over the test data and enhances the readability and maintainability of our test suite.

We believe that this approach provides a more robust and future-proof testing strategy for our project.