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

PHPUnit and Pest setup

Test setup for OPC UA-aware Symfony apps — disabling the daemon, the three test layers, fixtures, and the PHPUnit/Pest config.

The bundle plays well with both PHPUnit and Pest — the standard Symfony test-pack idioms apply.

Disable the daemon

The single most important rule: don't hit the daemon in unit or feature tests. Set in .env.test:

bash .env.test
OPCUA_SESSION_MANAGER_ENABLED=false

…paired with bundle config:

text config/packages/test/php_opcua_symfony_opcua.yaml
php_opcua_symfony_opcua:
    session_manager:
        enabled: false
    connections:
        default:
            endpoint:        'opc.tcp://localhost:14840'   # test server
            security_policy: None
            security_mode:   None

In the test environment, the bundle always uses direct mode — and you'll mock the client per test.

Three test layers

Layer Tests Touches OPC UA?
Unit Pure business logic No — mock everything
Functional Controllers, listeners, handlers No — mock the manager
Integration End-to-end against a real test server Yes (Docker fixture)

Unit + functional run on every push. Integration runs on PR merge or nightly.

PHPUnit configuration

text phpunit.dist.xml
<?xml version="1.0" encoding="UTF-8"?>
<phpunit
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xsi:noNamespaceSchemaLocation="https://schema.phpunit.de/11.0/phpunit.xsd"
    bootstrap="tests/bootstrap.php"
    colors="true"
    cacheDirectory=".phpunit.cache"
>
    <php>
        <env name="APP_ENV" value="test" force="true"/>
        <env name="SHELL_VERBOSITY" value="-1"/>
        <env name="OPCUA_SESSION_MANAGER_ENABLED" value="false" force="true"/>
    </php>

    <testsuites>
        <testsuite name="unit">
            <directory>tests/Unit</directory>
        </testsuite>
        <testsuite name="functional">
            <directory>tests/Functional</directory>
        </testsuite>
        <testsuite name="integration">
            <directory>tests/Integration</directory>
        </testsuite>
    </testsuites>
</phpunit>

Run a single suite:

bash terminal
vendor/bin/phpunit --testsuite=unit
vendor/bin/phpunit --testsuite=functional
vendor/bin/phpunit --testsuite=integration

Pest configuration

text tests/Pest.php
<?php

declare(strict_types=1);

use App\Tests\TestCase;
use App\Tests\KernelTestCase;
use App\Tests\IntegrationTestCase;

// Apply different base test classes per directory
uses(TestCase::class)->in('Unit');
uses(KernelTestCase::class)->in('Functional');
uses(IntegrationTestCase::class)->in('Integration');

beforeEach(function () {
    // Reset any global state if your unit tests need it.
});

Run:

bash terminal
vendor/bin/pest --group=unit
vendor/bin/pest --group=functional
vendor/bin/pest --group=integration

Unit tests — mock everything

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

use App\Service\SpeedService;
use PhpOpcua\Client\Testing\MockClient;
use PhpOpcua\Client\Types\DataValue;
use PHPUnit\Framework\TestCase;

final class SpeedServiceTest extends TestCase
{
    public function testCurrent(): void
    {
        $client = MockClient::create()
            ->onRead('ns=2;s=Speed', fn() => DataValue::ofDouble(75.0));

        $service = new SpeedService($client);

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

MockClient implements OpcUaClientInterface. See Using MockClient.

Functional tests — mock the manager

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

use PhpOpcua\Client\Testing\MockClient;
use PhpOpcua\Client\Types\DataValue;
use PhpOpcua\SymfonyOpcua\OpcuaManager;
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(OpcuaManager::class, new class($mock) extends OpcuaManager {
            public function __construct(private MockClient $mock)
            {
                parent::__construct([
                    'default' => 'default',
                    'connections' => ['default' => []],
                ]);
            }
            public function connect(?string $name = null): \PhpOpcua\Client\OpcUaClientInterface
            {
                return $this->mock;
            }
        });

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

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

See Mocking the manager and Kernel tests.

Integration tests — real server

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

use Symfony\Bundle\FrameworkBundle\Test\KernelTestCase;

abstract class IntegrationTestCase extends KernelTestCase
{
    protected static function getKernelClass(): string
    {
        return \App\Kernel::class;
    }
}
php tests/Integration/PlcReadTest.php
namespace App\Tests\Integration;

use PhpOpcua\SymfonyOpcua\OpcuaManager;

final class PlcReadTest extends IntegrationTestCase
{
    public function testReadServerStatus(): void
    {
        $opcua = static::getContainer()->get(OpcuaManager::class);
        assert($opcua instanceof OpcuaManager);

        $dv = $opcua->connect()->read('i=2256');

        $this->assertSame(0, $dv->statusCode);
    }
}

Run against a Docker-hosted test server — see Recipes · Dev with Docker.

CI matrix

text .github/workflows/test.yml
jobs:
  unit-and-functional:
    runs-on: ubuntu-latest
    strategy:
      matrix:
        php: ['8.2', '8.3', '8.4']
    steps:
      - uses: actions/checkout@v4
      - uses: shivammathur/setup-php@v2
        with: { php-version: '${{ matrix.php }}' }
      - run: composer install
      - run: vendor/bin/phpunit --testsuite=unit
      - run: vendor/bin/phpunit --testsuite=functional

  integration:
    runs-on: ubuntu-latest
    services:
      opcua:
        image: ghcr.io/php-opcua/uanetstandard-test-suite:v1.2.0
        ports: ['4840:4840', '4841:4841']
    steps:
      - uses: actions/checkout@v4
      - uses: shivammathur/setup-php@v2
        with: { php-version: '8.4' }
      - run: composer install
      - run: vendor/bin/phpunit --testsuite=integration

Unit/functional on every push; integration with a Docker service sidecar.

Useful test hooks

Disable cache between tests

php setUp
protected function setUp(): void
{
    parent::setUp();
    static::getContainer()->get('cache.app')->clear();
}

Freeze the clock

Symfony's Clock component:

text composer
composer require symfony/clock
php clock
use Symfony\Component\Clock\MockClock;
use Symfony\Component\Clock\Clock;

protected function setUp(): void
{
    Clock::set(new MockClock('2026-05-15 10:00:00'));
}

Fake the EventDispatcher

php event spy
use PhpOpcua\Client\Event\DataChangeReceived;

$dispatched = [];
static::getContainer()
    ->get('event_dispatcher')
    ->addListener(DataChangeReceived::class, function ($event) use (&$dispatched) {
        $dispatched[] = $event;
    });

// trigger code
// then assert on $dispatched

Coverage targets

Target Reasonable coverage
Services with OPC UA logic 90%+
Event listeners 90%+
Controllers 80%+
Trust-store / cert tooling Medium

Don't chase 100% — real-cert handshake testing is disproportionate.

Documentation