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

Using MockClient

MockClient — the in-memory OpcUaClientInterface implementation from opcua-client. Faithful behaviour without a network, suitable for high-coverage unit tests.

MockClient is a full in-memory implementation of the OpcUaClientInterface. It behaves like a real client (reads, writes, browses, subscriptions, errors) — but everything happens in PHP memory. No network, no daemon, no server.

When to reach for it

Scenario MockClient appropriate?
Unit-test a service that does 5+ reads/writes Yes
Test subscription callbacks Yes
Test code that handles write failures Yes
Feature-test a controller Usually facade mock instead
Integration-test against a real server No — use real daemon

MockClient shines when the code under test does multi-step OPC UA flows: read → conditionally write → re-read. Facade mocking gets verbose; MockClient reads cleanly.

Basic usage

php basic
use PhpOpcua\Client\Testing\MockClient;
use PhpOpcua\Client\Types\DataValue;

it('captures a reading', function () {
    $client = MockClient::create();
    $client->onRead('ns=2;s=Speed', fn() => DataValue::ofDouble(75.0));

    $service = new TagReadingService($client);
    $result = $service->getCurrentSpeed();

    expect($result)->toBe(75.0);
});

$client implements OpcUaClientInterface so anywhere your production code accepts that interface, the mock plugs in.

Programmable behaviour

php responses
$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', function (mixed $value) use ($client) {
    // Return true for success
    return 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) => /* CallResult */);

referenceFor() is a test helper — see the opcua-client testing reference.

Simulating errors

php error simulation
use PhpOpcua\Client\Exception\{ConnectionException, ServiceException};

$client->onRead('ns=2;s=Speed', function () {
    throw new ConnectionException('Network unreachable');
});

$client->onWrite('ns=2;s=Setpoint', function () {
    return false;     // returning false → ServiceException raised by the wrapper
});

Useful for testing retry logic and error reporting.

Subscriptions

MockClient mirrors the real createSubscription / createMonitoredItems surface — it does not expose a high-level callback-style subscribe() helper. To simulate a publish notification, dispatch a DataChangeReceived directly on the client's dispatcher (you can grab it via getEventDispatcher() or use Laravel's Event::dispatch(...) in a test that wires the mock through Laravel's container):

php simulate notification
use PhpOpcua\Client\Event\DataChangeReceived;

$client = MockClient::create();

$dispatcher = $client->getEventDispatcher();
$dispatcher->dispatch(new DataChangeReceived(
    client:         $client,
    subscriptionId: 1,
    sequenceNumber: 1,
    clientHandle:   1,
    dataValue:      DataValue::ofDouble(75.0),
));

The dispatched event reaches any listener registered via Event::listen(DataChangeReceived::class, ...).

Recording calls

MockClient exposes getCalls(), getCallsFor(string $method), callCount(string $method), and resetCalls() — see the opcua-client testing reference for the exact signatures. There are no getRecordedReads() / getRecordedWrites() helpers.

Assertion patterns:

php assert recorded
it('reads speed before writing setpoint', function () {
    $client = MockClient::create();
    $client->onRead('ns=2;s=Speed', fn() => DataValue::ofDouble(70.0));
    $client->onWrite('ns=2;s=Setpoint', fn() => 0);  // 0 = Good

    (new RecipeService($client))->bumpSetpoint();

    expect($client->callCount('read'))->toBeGreaterThan(0);
    expect($client->getCallsFor('write')[0])->toBeArray();
});

Binding to the container

OpcuaManager has no setMockConnection() method. To make MockClient reachable through the facade, override the manager binding with a manager subclass (or with a Mockery-mocked manager) that returns the mock client from connection():

php container binding
use PhpOpcua\LaravelOpcua\OpcuaManager;
use PhpOpcua\Client\OpcUaClientInterface;

beforeEach(function () {
    $this->mockClient = MockClient::create();

    $manager = Mockery::mock(OpcuaManager::class);
    $manager->shouldReceive('connection')->andReturn($this->mockClient);

    $this->app->instance(OpcuaManager::class, $manager);
});

it('still goes through the facade', function () {
    $this->mockClient->onRead('ns=2;s=Speed', fn() => DataValue::ofDouble(75.0));

    // Facade calls __call() which forwards to connection()
    // — the mocked manager returns the MockClient.
    $dv = Opcua::connection()->read('ns=2;s=Speed');

    expect($dv->getValue())->toBe(75.0);
});

For tests that exercise Opcua::read(...) directly through the facade's __call() magic, prefer facade mockingMockClient is most valuable when you can inject it into a service via constructor injection.

A reusable test trait

php HasMockOpcua trait
trait HasMockOpcua
{
    protected MockClient $mockClient;

    protected function setUpMockOpcua(): void
    {
        $this->mockClient = MockClient::create();

        $manager = Mockery::mock(OpcuaManager::class);
        $manager->shouldReceive('connection')->andReturn($this->mockClient);

        $this->app->instance(OpcuaManager::class, $manager);
    }
}

In a test:

php using the trait
uses(HasMockOpcua::class)->in('Feature/Plc');

it('reads via the facade', function () {
    $this->setUpMockOpcua();

    $this->mockClient->onRead('ns=2;s=Speed', fn() => DataValue::ofDouble(42.0));

    expect(Opcua::connection()->read('ns=2;s=Speed')->getValue())->toBe(42.0);
});

Mock vs facade — when to pick which

Test characteristic Pick
1-2 OPC UA calls, exact interaction known Facade shouldReceive
3+ calls, complex flow MockClient
Need recorded-call assertions MockClient (cleaner)
Testing a controller, not a service Facade shouldReceive
Testing a service with constructor injection MockClient

Mix freely. Most apps end up with a mix of both styles.

Comparing values

Don't compare DataValue instances with == — timestamps differ. Compare fields:

php value comparison
expect($dv->getValue())->toBe(75.0);
expect($dv->statusCode)->toBe(0);

Or use a custom expectation:

php custom expectation
// tests/Pest.php
expect()->extend('toBeGoodReading', function (mixed $expectedValue) {
    expect($this->value)->statusCode->toBe(0);
    expect($this->value)->value->toBe($expectedValue);
});

// In a test
expect($dv)->toBeGoodReading(75.0);
Documentation