Using MockClient
MockClient — the in-memory OpcUaClientInterface from opcua-client. Faithful behaviour without a network. The recommended approach for complex multi-call flows.
MockClient is a full in-memory OpcUaClientInterface. It
behaves like a real client (reads, writes, browses,
subscriptions, errors) — but everything happens in PHP memory.
When to reach for it
| Scenario | MockClient appropriate? |
|---|---|
| Unit-test a service with 5+ reads/writes | Yes |
| Test subscription callbacks | Yes |
| Test code that handles write failures | Yes |
| Functional test for a controller | Usually facade mock instead |
| Integration test against a real server | No — use real server |
MockClient shines when the test exercises multi-step flows.
Basic usage
use PhpOpcua\Client\Testing\MockClient;
use PhpOpcua\Client\Types\DataValue;
it('captures a reading', function () {
$client = MockClient::create()
->onRead('ns=2;s=Speed', fn() => DataValue::ofDouble(75.0));
$service = new \App\Service\SpeedService($client);
expect($service->current())->toBe(75.0);
});
MockClient implements OpcUaClientInterface — anywhere your
production code accepts that interface, the mock plugs in.
Programmable behaviour
$client = MockClient::create();
// Reads
$client->onRead('ns=2;s=Speed', fn() => DataValue::ofDouble(75.0));
$client->onRead('ns=2;s=Temperature', fn() => DataValue::ofDouble(22.5));
// Writes — capture for assertion, optional success/fail
$client->onWrite('ns=2;s=Setpoint', fn(mixed $value) => true);
// Browse — return a fixture set
$client->onBrowse('ns=2;s=Folder', fn() => [
referenceFor('ns=2;s=Folder.Speed'),
referenceFor('ns=2;s=Folder.Temperature'),
]);
// Method calls
$client->onCall('ns=2;s=Recipe', 'ns=2;s=Recipe.Load',
fn(array $inputs) => [0, [true]]);
Simulating errors
use PhpOpcua\Client\Exception\ConnectionException;
$client->onRead('ns=2;s=Speed', function () {
throw new ConnectionException('Network unreachable');
});
$client->onWrite('ns=2;s=Setpoint', fn() => false); // raises ServiceException
Subscriptions
MockClient does not expose a fluent subscribe() / monitor()
surface — those methods don't exist on OpcUaClientInterface.
Drive subscription-style tests by directly dispatching the real
event classes onto the EventDispatcher under test:
use PhpOpcua\Client\Event\DataChangeReceived;
use PhpOpcua\Client\Testing\MockClient;
use PhpOpcua\Client\Types\DataValue;
use Symfony\Component\EventDispatcher\EventDispatcher;
$client = MockClient::create();
$dispatcher = new EventDispatcher();
// Register your application listener(s) on $dispatcher here.
$event = new DataChangeReceived(
client: $client,
subscriptionId: 1,
sequenceNumber: 1,
clientHandle: 7,
dataValue: DataValue::ofDouble(75.0),
);
$dispatcher->dispatch($event);
Recorded calls
MockClient exposes a call recorder (getCalls, getCallsFor,
callCount, resetCalls) rather than per-operation
getRecordedReads() / getRecordedWrites() helpers:
$client->callCount('read'); // int
$client->getCallsFor('write'); // each call's args
$client->resetCalls(); // start fresh
In a test:
it('reads speed before writing setpoint', function () {
$client = MockClient::create()
->onRead(function (string|\PhpOpcua\Client\Types\NodeId $nodeId) {
if ((string) $nodeId === 'ns=2;s=Speed') {
return DataValue::ofDouble(70.0);
}
})
->onWrite(fn() => 0);
(new \App\Service\RecipeService($client))->bumpSetpoint();
expect($client->callCount('read'))->toBe(1);
expect($client->callCount('write'))->toBe(1);
$writeArgs = $client->getCallsFor('write')[0];
expect((string) $writeArgs[0])->toBe('ns=2;s=Setpoint');
});
Binding to the container
In a WebTestCase, swap into the container:
use PhpOpcua\Client\OpcUaClientInterface;
use PhpOpcua\Client\Testing\MockClient;
protected function setUp(): void
{
parent::setUp();
static::createClient();
$this->mock = MockClient::create();
static::getContainer()->set(OpcUaClientInterface::class, $this->mock);
}
Now any controller injecting OpcUaClientInterface gets the
mock.
A reusable test trait
namespace App\Tests;
use PhpOpcua\Client\OpcUaClientInterface;
use PhpOpcua\Client\Testing\MockClient;
trait HasMockOpcua
{
protected MockClient $mock;
protected function setUpOpcuaMock(): void
{
$this->mock = MockClient::create();
static::getContainer()->set(OpcUaClientInterface::class, $this->mock);
}
}
Usage:
final class SomeControllerTest extends WebTestCase
{
use HasMockOpcua;
public function testIt(): void
{
static::createClient();
$this->setUpOpcuaMock();
$this->mock->onRead('ns=2;s=Speed', fn() => DataValue::ofDouble(42.0));
// exercise + assert
}
}
Mock vs manager — when to pick which
| Test | Pick |
|---|---|
| Single OPC UA call, exact interaction known | Manager mock |
| 3+ calls, complex flow | MockClient |
| Need recorded-call assertions | MockClient |
| Test a controller, not a service | Manager mock |
Test a service with OpcUaClientInterface injection |
MockClient |
Mix freely. Most apps end up with both styles.
Comparing values
Don't compare DataValue instances with == — timestamps
differ. Compare fields:
expect($dv->getValue())->toBe(75.0);
expect($dv->statusCode)->toBe(0);
Or a custom expectation:
// tests/Pest.php
expect()->extend('toBeGoodReading', function (mixed $expectedValue) {
expect($this->value)->statusCode->toBe(0);
expect($this->value->getValue())->toBe($expectedValue);
});
// usage
expect($dv)->toBeGoodReading(75.0);
Where to read next
- Kernel tests —
KernelTestCasepatterns. - opcua-client testing — the upstream MockClient reference.