Additionally/Testing

Testing

The framework structure is designed to avoid any obstacles to code testing built on it. This applies to all types of controllers, standard services, and custom framework functions.

The testing approach depends on the usage type of the services, which may be a corresponding class with static methods such as Hleb\Static\Service::method() for built-in framework services, or DI, referring to service (and other object) injection into class methods and constructors.

Dependency Injection within the framework is limited to objects created by it, including controllers, middleware, commands, events, and objects created by the service known as DI.


#Testing for Dependency Injection

A simple example of a demonstration controller with DI:

<?php

namespace App\Controllers;

use 
Hleb\Base\Controller;
use 
Hleb\Reference\Interface\Log;

class 
ExampleController extends Controller
{
    public function 
index(Log $logger): string
    
{
        
$logger->info('Request to demo controller');

        return 
'OK';
    }
}

Suppose you need to ensure that the controller returns the text 'OK' without sending a message to the logs.

use App\Controllers\ExampleController;
use 
Hleb\Main\Logger\NullLogger;

$controller = new ExampleController();
$logger = new NullLogger();
$result $controller->index($logger);

if (
$result === 'OK') {
    
// Successful test.
}

Here, the logging class is replaced by a class with the same interface, but its methods do not send anything to the log.

It is assumed that one of the special testing libraries (such as github.com/phhleb/test-o) is used, with checks implemented through it.

Now, let’s invoke the method of an arbitrary class through the DI service (specifically the framework service, not the architectural pattern itself):

use Hleb\Reference\Interface\Log;

class 
Example
{
    public function 
run(Log $logger): string
    
{
        
$logger->info('Demo class method executed');

        return 
'OK';
    }
}

use 
Hleb\Static\DI;

$result DI::method(new Example(), 'run');

In this case, the logging service will be injected from the container, and the message will be logged. Let’s modify the method invocation for testing:

use Hleb\Main\Logger\NullLogger;
use 
Hleb\Static\DI;

$result DI::method(new Example(), 'run', ['logger' => new NullLogger()]);

if (
$result === 'OK') {
    
// Successful test.
}

Now the class has been tested without logging occurring. You can substitute any DI object with a custom class designed for the required behavior, making it convenient for testing.


#Testing Standard Services

The built-in services of the HLEB2 framework can be accessed with static methods such as Hleb\Static\Service::method(). This approach simplifies access to services but can complicate testing of the modules containing them, although it is still feasible. Here's an example with logging:

use Hleb\Static\Log;

class 
Example
{
    public function 
run(): string
    
{
        
Log::info('Demo class method executed');

        return 
'OK';
    }
}

use 
Hleb\Main\Logger\NullLogger;
use 
Hleb\Init\ShootOneselfInTheFoot\LogForTest;

$logger = new NullLogger();

LogForTest::set($logger);

$result = (new Example())->run();

LogForTest::cancel();

if (
$result === 'OK') {
    
// Successful test.
}

The example shows how the service state was replaced with a test object and then reverted to its initial value. To prevent this approach from being used outside of tests, in a production project, the configuration parameter 'container.mock.allowed' in the /config/common.php file is set to false.


#Functional Testing

To run tests that initialize the core of the framework, you may need to replace some or all services in the container with test objects. To do this, simply implement your own service and assign it based on a condition (in the example, this is the global constant APP_TEST_ON):

<?php
// File /app/Bootstrap/BaseContainer.php

namespace App\Bootstrap;

use 
Hleb\Constructor\Containers\CoreContainer;

final class 
BaseContainer extends CoreContainer implements ContainerInterface
{
    private ?
ContainerInterface $testContainer null;

    #[\Override]
    final public function 
get(string $id): mixed
    
{
        if (
get_constant('APP_TEST_ON')) {
            if (
$this->testContainer === null) {
                
$this->testContainer = new TestContainer();
            }
            return 
$this->testContainer->get($id);
        }

        return 
ContainerFactory::getSingleton($id) ?? match ($id) {

            
// ... //

            
default => parent::get($id),
        };
    }
}

#Testing Built-in Functions

Several built-in framework functions that simplify service calls, such as the logger() function, are implemented through tested service calls, in this case, as a wrapper around Hleb\Static\Log.


#Testing for $this-container in Classes

In controllers, middlewares, commands, events, and other classes inherited from Hleb\Base\Container, the container can be accessed as $this-container. If you choose this method of using the container (mixing various methods within a project would look odd), special initialization of the object constructor is required for testing.

use Hleb\Base\Container;
use 
Hleb\Reference\LogInterface;

class 
Example extends Container
{
    public function 
run(): string
    
{
        
$this->container->get(LogInterface::class)->info('Demo class method executed');

        return 
'OK';
    }
}
// TestContainer has an interface App\Bootstrap\ContainerInterface.
$config = ['container' => new TestContainer()];

$result = (new Example($config))->run();

if (
$result === 'OK') {
    
// Successful test.
}
Page translated: chatgpt 4-o
Back to top