Mocking the manager
Replace OpcuaManager / OpcUaClientInterface in the Symfony container per test. Mockery and Prophecy patterns; container::set; container::override.
In Symfony tests, replace the bundle's services in the test container. Two approaches:
static::getContainer()->set()— the modern Symfony idiom.- Mockery / PHPUnit createMock() — for service-isolated unit tests.
static::getContainer()->set() — recommended
Symfony's test container allows overriding any service. Per test, swap in a mock:
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));
// Replace OpcUaClientInterface in the container with the mock
static::getContainer()->set(OpcUaClientInterface::class, $mock);
$client->request('GET', '/api/plc/speed');
$this->assertResponseIsSuccessful();
}
}
This works because the controller injects
OpcUaClientInterface — and the container now returns the
mock.
Replacing OpcuaManager
When the code under test uses the manager directly:
use PhpOpcua\Client\Testing\MockClient;
use PhpOpcua\Client\Types\DataValue;
use PhpOpcua\SymfonyOpcua\OpcuaManager;
public function testService(): void
{
$client = static::createClient();
$mock = MockClient::create()
->onRead('ns=2;s=Speed', fn() => DataValue::ofDouble(75.0));
// Anonymous subclass returns the mock for any connection name
$manager = new class($mock) extends OpcuaManager {
public function __construct(private MockClient $m) {
parent::__construct(['default' => 'd', 'connections' => ['d' => []]]);
}
public function connect(?string $name = null): \PhpOpcua\Client\OpcUaClientInterface { return $this->m; }
};
static::getContainer()->set(OpcuaManager::class, $manager);
$client->request('GET', '/api/plc/speed');
$this->assertResponseIsSuccessful();
}
The anonymous subclass keeps the test inline; for several tests
that share the pattern, extract a TestableOpcuaManager class.
Multi-connection mocking
When the code switches between connections:
$lineAMock = MockClient::create()
->onRead('ns=2;s=Speed', fn() => DataValue::ofDouble(75.0));
$lineBMock = MockClient::create()
->onRead('ns=2;s=Speed', fn() => DataValue::ofDouble(80.0));
$manager = new class($lineAMock, $lineBMock) extends OpcuaManager {
public function __construct(
private MockClient $a,
private MockClient $b,
) {
parent::__construct([
'default' => 'plc-line-a',
'connections' => [
'plc-line-a' => [],
'plc-line-b' => [],
],
]);
}
public function connect(?string $name = null): \PhpOpcua\Client\OpcUaClientInterface
{
return match ($name ?? 'plc-line-a') {
'plc-line-a' => $this->a,
'plc-line-b' => $this->b,
default => throw new \InvalidArgumentException("Unknown: $name"),
};
}
};
static::getContainer()->set(OpcuaManager::class, $manager);
Mockery — when MockClient isn't enough
MockClient covers most cases, but for complex assertion needs
(call count, argument matching), use Mockery:
use Mockery as M;
use PhpOpcua\Client\OpcUaClientInterface;
use PhpOpcua\Client\Types\DataValue;
protected function tearDown(): void
{
M::close();
parent::tearDown();
}
public function testWriteCallsServer(): void
{
$client = static::createClient();
$mock = M::mock(OpcUaClientInterface::class);
$mock->shouldReceive('write')
->once()
->with('ns=2;s=Setpoint', 75.0)
->andReturn(0); // OPC UA Good status
static::getContainer()->set(OpcUaClientInterface::class, $mock);
$client->request('PUT', '/api/plc/ns=2;s=Setpoint', [], [], [], json_encode(['value' => 75.0]));
$this->assertResponseIsSuccessful();
}
PHPUnit's createMock()
For service-isolated unit tests (no container):
use PhpOpcua\Client\OpcUaClientInterface;
use PhpOpcua\Client\Types\DataValue;
use PHPUnit\Framework\TestCase;
final class SpeedServiceTest extends TestCase
{
public function testCurrent(): void
{
$dv = $this->createMock(DataValue::class);
$dv->method('getValue')->willReturn(75.0);
$dv->statusCode = 0;
$client = $this->createMock(OpcUaClientInterface::class);
$client->method('read')->with('ns=2;s=Speed')->willReturn($dv);
$service = new \App\Service\SpeedService($client);
$this->assertSame(75.0, $service->current());
}
}
Pest expectations
use App\Service\SpeedService;
use PhpOpcua\Client\Testing\MockClient;
use PhpOpcua\Client\Types\DataValue;
it('returns the current speed', function () {
$client = MockClient::create()
->onRead('ns=2;s=Speed', fn() => DataValue::ofDouble(75.0));
$service = new SpeedService($client);
expect($service->current())->toBe(75.0);
});
Throwing exceptions
For testing error paths:
use PhpOpcua\Client\Exception\ConnectionException;
use PhpOpcua\Client\Testing\MockClient;
$mock = MockClient::create()
->onRead('ns=2;s=Speed', function () {
throw new ConnectionException('PLC unreachable');
});
static::getContainer()->set(OpcUaClientInterface::class, $mock);
$client->request('GET', '/api/plc/speed');
$this->assertResponseStatusCodeSame(502);
Spying on events
Test that the bundle dispatched an event:
use PhpOpcua\Client\Event\DataChangeReceived;
use Symfony\Component\EventDispatcher\EventDispatcherInterface;
$dispatcher = static::getContainer()->get('event_dispatcher');
$received = [];
$dispatcher->addListener(DataChangeReceived::class, function (DataChangeReceived $e) use (&$received) {
$received[] = $e;
});
// trigger code path that should emit
$this->assertCount(1, $received);
$this->assertSame(1, $received[0]->clientHandle); // event has clientHandle, not nodeId
When manager mocking is wrong
Two cases:
- Testing the manager itself. Don't — it's tested upstream.
- Complex multi-call flows. A test with 8
shouldReceivecalls signals too much coupling — refactor the production code first.
For complex flows, prefer MockClient — see
Using MockClient.
Container override permissions
Replacing services requires that the service is non-public
turned public for tests. Symfony's test container does this
automatically for any service listed in
config/services_test.yaml:
services:
test.OpcUaClientInterface:
alias: PhpOpcua\Client\OpcUaClientInterface
public: true
test.OpcuaManager:
alias: PhpOpcua\SymfonyOpcua\OpcuaManager
public: true
Modern Symfony defaults to making all services public in the test container — usually you don't need this.
Where to read next
- Using MockClient — when manager mocking gets verbose.
- Kernel tests — full
KernelTestCasepatterns.