Testing

Prerequisites

Integration tests require the OPC UA test server suite running locally:

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

The test suite provides 8 OPC UA servers on ports 4840–4847 with different security configurations.

Running Tests

# All tests
./vendor/bin/pest

# Unit tests only (no server or daemon required)
./vendor/bin/pest tests/Unit/

# Integration tests only (requires test servers)
./vendor/bin/pest tests/Integration/ --group=integration

# A specific test file
./vendor/bin/pest tests/Unit/TypeSerializerTest.php

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

Coverage Note

SessionManagerDaemon is excluded from coverage reports (see codecov.yml). PHP coverage tools (pcov, xdebug) only instrument the process that runs the test suite — they cannot track code executing inside a child process started via proc_open().

The daemon IS fully tested by the integration test suite: TestHelper::startDaemon() starts a real daemon process, the tests send real IPC commands and verify real responses. The coverage tool simply cannot see into the subprocess.

This is a known limitation shared by other daemon/worker-based PHP packages:

  • Laravel Horizon — Redis queue workers run as child processes, not covered by PHPUnit
  • Symfony Messenger — consumer workers are separate processes
  • ReactPHP servers — the event loop runs in a standalone process
  • RoadRunner / FrankenPHP — PHP workers are spawned by the Go runtime

Without the exclusion, reported coverage would be ~82% instead of the actual ~99%+ that the full test suite provides.

Test Structure

Unit Tests

File Tests Covers
TypeSerializerTest.php 40+ All type serialization/deserialization roundtrips including v4.0.0 DTOs
SessionStoreTest.php 8 Session CRUD, expiration, touching
ManagedClientConfigTest.php 17 Configuration setters/getters, logger, cache, extension object repository
CommandHandlerSecurityTest.php 18 Method whitelist, setter rejection, credential stripping, max sessions, cert validation
BrowseDirectionSerializationTest.php 8 BrowseDirection, ConnectionState, BrowseNode tree serialization
AutoGeneratedCertTest.php 7 Auto-generated certificate flow, cert path validation bypass

Integration Tests

File Tests Covers
DaemonTest.php Ping, list, open/close, error handling
ConnectionTest.php Anonymous, username/password, certificate, invalid host
ConnectionStateTest.php isConnected, getConnectionState, reconnect
BrowseTest.php Browse with direction, reference types, continuation
BrowseRecursiveTest.php browseAll, browseRecursive, depth limits
ReadTest.php Single/multi read, all scalar types
WriteTest.php Single/multi write, all types, batching
MethodCallTest.php Method invocation with arguments
SubscriptionTest.php Create, monitor, publish, delete
TranslateBrowsePathTest.php Path resolution, namespace paths
TimeoutAndBatchingTest.php Timeout, batching, server limits
SessionPersistenceTest.php Cross-instance, state isolation
SecurityTest.php Auth token, method whitelist, credential stripping
AutoCertConnectionTest.php Auto-generated certificates

TestHelper

The TestHelper class manages daemon lifecycle and provides client utilities:

use PhpOpcua\SessionManager\Tests\Integration\Helpers\TestHelper;

// Daemon lifecycle
TestHelper::startDaemon();      // starts daemon on test socket
TestHelper::stopDaemon();       // graceful shutdown
TestHelper::isDaemonRunning();  // check status

// Client helpers
$client = TestHelper::createManagedClient();
$client = TestHelper::connectNoSecurity();
$nodeId = TestHelper::browseToNode($client, ['TestServer', 'DataTypes', 'Scalar']);
$ref = TestHelper::findRefByName($refs, 'BooleanValue');
TestHelper::safeDisconnect($client);

Usage in Tests

beforeAll(fn() => TestHelper::startDaemon());
afterAll(fn() => TestHelper::stopDaemon());

it('reads a value', function () {
    $client = null;
    try {
        $client = TestHelper::connectNoSecurity();
        $value = $client->read(NodeId::numeric(0, 2259));
        expect($value->statusCode)->toBe(0);
    } finally {
        TestHelper::safeDisconnect($client);
    }
})->group('integration');

Test Endpoints

Constant Port Description
ENDPOINT_NO_SECURITY 4840 Anonymous, no encryption
ENDPOINT_USERPASS 4841 Username/password authentication
ENDPOINT_CERTIFICATE 4842 Certificate authentication
ENDPOINT_ALL_SECURITY 4843 All security options
ENDPOINT_DISCOVERY 4844 Endpoint discovery
ENDPOINT_AUTO_ACCEPT 4845 Auto-accept certificates
ENDPOINT_SIGN_ONLY 4846 Sign-only mode
ENDPOINT_LEGACY 4847 Legacy security policies