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:
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:
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:
framework:
messenger:
transports:
async_opcua: 'sync://'
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:
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:
$dispatcher = static::getContainer()->get('event_dispatcher');
$dispatcher->dispatch($event);
Integration tests against a real server
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:
composer require --dev dama/doctrine-test-bundle
<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:
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:
framework:
secret: test
php_opcua_symfony_opcua:
connections:
default:
endpoint: 'opc.tcp://localhost:14840'
Boot in tests:
$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.
Where to read next
You've finished Testing. Next: Integrations · Messenger for async patterns.