symfony-opcua · v4.3.x
Docs · Testing

Kernel tests

KernelTestCase + WebTestCase + integration tests — the Symfony test-pack idioms applied to OPC UA-driven apps.

For tests that need the Symfony container — controllers, services, Messenger handlers — use Symfony's KernelTestCase / WebTestCase.

KernelTestCase — without HTTP

For testing services that depend on the container:

php tests/Functional/SpeedServiceTest.php
namespace App\Tests\Functional;

use App\Service\SpeedService;
use PhpOpcua\Client\OpcUaClientInterface;
use PhpOpcua\Client\Testing\MockClient;
use PhpOpcua\Client\Types\DataValue;
use Symfony\Bundle\FrameworkBundle\Test\KernelTestCase;

final class SpeedServiceTest extends KernelTestCase
{
    public function testCurrent(): void
    {
        self::bootKernel();

        $mock = MockClient::create()
            ->onRead('ns=2;s=Speed', fn() => DataValue::ofDouble(75.0));

        static::getContainer()->set(OpcUaClientInterface::class, $mock);

        $service = static::getContainer()->get(SpeedService::class);

        $this->assertSame(75.0, $service->current());
    }
}

bootKernel() initialises the Symfony kernel; the container is available via getContainer().

WebTestCase — with HTTP

For controllers:

php tests/Functional/SpeedControllerTest.php
namespace App\Tests\Functional;

use PhpOpcua\Client\OpcUaClientInterface;
use PhpOpcua\Client\Testing\MockClient;
use PhpOpcua\Client\Types\DataValue;
use Symfony\Bundle\FrameworkBundle\Test\WebTestCase;

final class SpeedControllerTest extends WebTestCase
{
    public function testShow(): void
    {
        $client = static::createClient();

        $mock = MockClient::create()
            ->onRead('ns=2;s=Speed', fn() => DataValue::ofDouble(75.0));
        static::getContainer()->set(OpcUaClientInterface::class, $mock);

        $client->request('GET', '/api/plc/speed');

        $this->assertResponseIsSuccessful();
        $this->assertJsonStringEqualsJsonString(
            json_encode(['value' => 75.0, 'good' => true, 'at' => null]),
            $client->getResponse()->getContent(),
        );
    }
}

createClient() boots the kernel and returns a KernelBrowser for HTTP requests.

Testing Messenger handlers

Symfony Messenger's sync transport is perfect for tests — messages are handled synchronously.

config/packages/test/messenger.yaml:

text test transport
framework:
    messenger:
        transports:
            async_opcua: 'sync://'

Test:

php handler test
namespace App\Tests\Functional;

use App\Entity\PlcReading;
use App\Message\StoreReading;
use Doctrine\ORM\EntityManagerInterface;
use PhpOpcua\Client\OpcUaClientInterface;
use PhpOpcua\Client\Testing\MockClient;
use PhpOpcua\Client\Types\DataValue;
use Symfony\Bundle\FrameworkBundle\Test\KernelTestCase;
use Symfony\Component\Messenger\MessageBusInterface;

final class StoreReadingHandlerTest extends KernelTestCase
{
    public function testStoresReading(): void
    {
        self::bootKernel();

        $bus = static::getContainer()->get(MessageBusInterface::class);
        $em  = static::getContainer()->get(EntityManagerInterface::class);

        $bus->dispatch(new StoreReading(
            connection: 'default',
            nodeId:     'ns=2;s=Speed',
            value:      75.0,
            statusCode: 0,
            at:         new \DateTimeImmutable(),
        ));

        $readings = $em->getRepository(PlcReading::class)->findAll();
        $this->assertCount(1, $readings);
        $this->assertSame(75.0, $readings[0]->getValue());
    }
}

sync:// runs handlers in the same process, so the assertCount works without async polling.

Testing event listeners

For listeners that dispatch on DataChangeReceived:

php listener test
namespace App\Tests\Functional;

use App\EventListener\StoreSpeedReading;
use PhpOpcua\Client\Event\DataChangeReceived;
use PhpOpcua\Client\Testing\MockClient;
use PhpOpcua\Client\Types\DataValue;
use Symfony\Bundle\FrameworkBundle\Test\KernelTestCase;

final class StoreSpeedReadingTest extends KernelTestCase
{
    public function testOnDataChange(): void
    {
        self::bootKernel();

        $listener = static::getContainer()->get(StoreSpeedReading::class);

        // DataChangeReceived's public constructor:
        // ($client, $subscriptionId, $sequenceNumber, $clientHandle, $dataValue)
        $event = new DataChangeReceived(
            client:          MockClient::create(),
            subscriptionId:  1,
            sequenceNumber:  1,
            clientHandle:    1,
            dataValue:       DataValue::ofDouble(75.0),
        );

        $listener($event);

        // Assert on the side-effect (DB row, broadcast, etc.)
    }
}

Direct invocation is cleaner than dispatching through the EventDispatcher, but the latter works too:

php via dispatcher
$dispatcher = static::getContainer()->get('event_dispatcher');
$dispatcher->dispatch($event);

Integration tests against a real server

php tests/Integration/PlcReadTest.php
namespace App\Tests\Integration;

use PhpOpcua\SymfonyOpcua\OpcuaManager;
use Symfony\Bundle\FrameworkBundle\Test\KernelTestCase;

final class PlcReadTest extends KernelTestCase
{
    public function testReadServerStatus(): void
    {
        self::bootKernel();

        $opcua = static::getContainer()->get(OpcuaManager::class);
        $dv = $opcua->connect()->read('i=2256');

        $this->assertSame(0, $dv->statusCode);
        $this->assertSame(0, $dv->getValue());   // 0 = Running
    }
}

Pair with a Docker php-opcua/uanetstandard-test-suite test server — see Recipes · Dev with Docker.

Doctrine in functional tests

Reset the DB per test with dama/doctrine-test-bundle:

text composer
composer require --dev dama/doctrine-test-bundle
text phpunit.dist.xml
<extensions>
    <bootstrap class="DAMA\DoctrineTestBundle\PHPUnit\PHPUnitExtension"/>
</extensions>

Each test gets a transaction-rollback-wrapped DB — no data leaks between tests.

Multiple containers — symfony-opcua bundle test

If you're testing the bundle itself (not an app using it), boot a small test kernel:

php bundle test kernel
namespace PhpOpcua\SymfonyOpcua\Tests;

use PhpOpcua\SymfonyOpcua\PhpOpcuaSymfonyOpcuaBundle;
use Symfony\Bundle\FrameworkBundle\FrameworkBundle;
use Symfony\Component\Config\Loader\LoaderInterface;
use Symfony\Component\HttpKernel\Kernel;

final class TestKernel extends Kernel
{
    public function registerBundles(): iterable
    {
        return [
            new FrameworkBundle(),
            new PhpOpcuaSymfonyOpcuaBundle(),
        ];
    }

    public function registerContainerConfiguration(LoaderInterface $loader): void
    {
        $loader->load(__DIR__ . '/fixtures/config.yaml');
    }
}

…with fixtures/config.yaml:

text config
framework:
    secret: test

php_opcua_symfony_opcua:
    connections:
        default:
            endpoint: 'opc.tcp://localhost:14840'

Boot in tests:

php boot the test kernel
$kernel = new TestKernel('test', false);
$kernel->boot();
$container = $kernel->getContainer();

CI matrix

See PHPUnit and Pest setup for the GitHub Actions matrix.

Performance

Test type Per-test budget
Unit < 50 ms
Functional < 500 ms
Integration < 5 s

createClient() is the slow part of functional tests — reuse the booted kernel where possible.

You've finished Testing. Next: Integrations · Messenger for async patterns.

Documentation