Testing

MockClient

The underlying opcua-client provides a MockClient — a drop-in test double that implements OpcUaClientInterface with no TCP connection. Use it to test your application logic without a real OPC UA server.

Basic Usage

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

$mock = MockClient::create()
    ->onRead('i=2259', fn() => DataValue::ofInt32(0))
    ->onRead('ns=2;s=Temperature', fn() => DataValue::ofDouble(23.5));

$value = $mock->read('i=2259');
echo $value->getValue(); // 0

Register Handlers

$mock = MockClient::create()
    ->onRead($nodeId, fn() => DataValue::ofInt32(42))
    ->onWrite($nodeId, fn() => StatusCode::Good)
    ->onBrowse($nodeId, fn() => [...$references])
    ->onCall($objectId, $methodId, fn(array $args) => new CallResult(...))
    ->onResolveNodeId($path, fn() => new NodeId(0, 2253))
    ->onGetEndpoints(fn() => [...$endpoints]);  // v4.0+

v4.0+ MockClient Methods

The MockClient in v4 supports additional methods for testing trust store, event, and subscription features:

$mock = MockClient::create()
    // Event dispatcher
    ->setEventDispatcher($dispatcher)

    // Endpoint discovery handler
    ->onGetEndpoints(fn() => [...$endpoints])

    // Write type auto-detection (type parameter is now optional)
    ->setAutoDetectWriteType(true)

    // Read metadata cache
    ->setReadMetadataCache(true)

    // Trust store
    ->setTrustStorePath('/tmp/test-trust')
    ->setTrustPolicy(TrustPolicy::Fingerprint);

Additional operations available on the mock:

// Modify monitored items
$mock->modifyMonitoredItems($subId, [
    ['monitoredItemId' => 1, 'samplingInterval' => 200.0],
]);

// Set triggering links
$mock->setTriggering($subId, $triggeringItemId, [2, 3], []);

Note: In v4, the write() type parameter is nullable. MockClient reflects this -- you can call $mock->write('ns=2;i=1001', 42) without specifying a type, and it will use the onWrite handler as usual.

Call Tracking

$mock->read('i=2259');
$mock->read('i=2256');

echo $mock->callCount('read');       // 2
echo count($mock->getCalls());       // 2
echo count($mock->getCallsFor('read')); // 2

$mock->resetCalls();
echo $mock->callCount('read');       // 0

DataValue Factory Methods

DataValue::ofBoolean(true);
DataValue::ofInt32(42);
DataValue::ofUInt32(100);
DataValue::ofDouble(3.14);
DataValue::ofString('hello');
DataValue::ofFloat(2.5);
DataValue::of($value, BuiltinType::Int32);
DataValue::bad(StatusCode::BadNodeIdUnknown);

Injecting MockClient into OpcuaManager

use PhpOpcua\SymfonyOpcua\OpcuaManager;

$mock = MockClient::create()
    ->onRead('i=2259', fn() => DataValue::ofInt32(0));

$manager = new OpcuaManager([
    'default' => 'default',
    'session_manager' => ['enabled' => false, 'socket_path' => '/tmp/nonexistent.sock'],
    'connections' => ['default' => ['endpoint' => 'opc.tcp://localhost:4840']],
]);

// Replace the connection with the mock
$ref = new \ReflectionProperty($manager, 'connections');
$ref->setValue($manager, ['default' => $mock]);

$value = $manager->connection('default')->read('i=2259');
echo $value->getValue(); // 0
echo $mock->callCount('read'); // 1

Running Tests

Unit Tests

No external dependencies required:

./vendor/bin/pest tests/Unit/

Integration Tests

Require the uanetstandard-test-suite Docker containers:

git clone https://github.com/php-opcua/uanetstandard-test-suite.git
cd uanetstandard-test-suite
docker compose up -d

Then run:

./vendor/bin/pest tests/Integration/ --group=integration

All Tests

./vendor/bin/pest

Coverage

php -d pcov.enabled=1 ./vendor/bin/pest --coverage

Target: 99.5%+ code coverage.

Test Structure

tests/
├── Unit/
│   ├── ConfigTest.php                   # YAML config validation
│   ├── MockClientTest.php               # MockClient integration
│   ├── OpcuaManagerTest.php             # Manager logic, configureBuilder
│   └── PhpOpcuaSymfonyOpcuaBundleTest.php # Bundle registration, DI wiring
└── Integration/
    ├── Helpers/
    │   └── TestHelper.php               # Endpoints, config builders, daemon management
    ├── ConnectionTest.php               # Direct and managed connections
    ├── ConnectionStateTest.php          # isConnected, reconnect
    ├── ReadTest.php                     # Scalar, array, multi-read
    ├── WriteTest.php                    # Scalar, array, multi-write
    ├── BrowseTest.php                   # Browse, continuation, NodeClass filter
    ├── BrowseRecursiveTest.php          # browseAll, browseRecursive, maxDepth
    ├── TranslateBrowsePathTest.php      # resolveNodeId, translateBrowsePaths
    ├── MethodCallTest.php               # Method invocation
    ├── SubscriptionTest.php             # Create, monitor, publish, delete
    ├── SubscriptionTransferTest.php     # transferSubscriptions, republish
    ├── HistoryReadAdvancedTest.php       # historyReadRaw, historyReadAtTime
    ├── TimeoutTest.php                  # Timeout config and fluent API
    ├── AutoRetryTest.php                # Auto-retry config and fluent API
    ├── BatchingTest.php                 # Server limits, transparent batching
    ├── StringNodeIdTest.php             # String NodeId in read, browse, readMulti
    ├── BuilderApiTest.php               # Fluent builders for read, write, paths
    ├── CacheTest.php                    # setCache, invalidateCache, flushCache, useCache
    ├── LoggerTest.php                   # setLogger, getLogger
    ├── TypeDiscoveryTest.php            # discoverDataTypes, getExtensionObjectRepository
    ├── DynamicVariableTest.php          # Counter, Random, SineWave, Timestamp
    └── ErrorHandlingTest.php            # BadNodeIdUnknown, read-only writes

TestHelper

The TestHelper class provides:

  • Endpoint constants — 8 test server endpoints (no-security, userpass, certificate, auto-accept, etc.)
  • Daemon managementstartDaemon() / stopDaemon() for integration tests
  • Config buildersmakeDirectConfig(), makeManagedConfig(), createDirectManager(), createManagedManager()
  • Browse helpersbrowseToNode(), findRefByName(), safeDisconnect()

All integration tests run in both direct and managed (daemon) modes using a foreach loop pattern.