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

Integration tests

Integration tests run against real OPC UA servers in Docker. Two sibling repos cover the spectrum — UA-.NETStandard for almost everything, open62541 for NodeManagement.

The library's own integration suite — tests/Integration/ in this repo — runs against two Docker-based reference servers. The same servers are useful when you're testing your own integration code; the images, the compose files, and the port assignments are all public.

The reference servers

Stack Source Coverage
UA-.NETStandard php-opcua/uanetstandard-test-suite Eight endpoints — every security policy, both modes, anonymous + username + certificate auth, custom DataTypes, ECC variants on :4848/:4849
open62541 php-opcua/extra-test-suite One endpoint with NodeManagement enabled on :24840 — the only counterpart needed for that service set

Both ship pre-built images to GHCR. Local startup is docker compose pull && docker compose up -d, no build step required.

Port map

The library's TestHelper::ENDPOINT_* constants encode this directly — same constants you can reuse in your own tests:

Endpoint constant URL What
ENDPOINT_NO_SECURITY opc.tcp://localhost:4840 None policy, anonymous
ENDPOINT_SIGN opc.tcp://localhost:4841 Basic256Sha256 + Sign
ENDPOINT_SIGN_AND_ENCRYPT opc.tcp://localhost:4842 Basic256Sha256 + SignAndEncrypt
ENDPOINT_USERPASS opc.tcp://localhost:4843 Username/password auth
ENDPOINT_CERT opc.tcp://localhost:4844 X.509 certificate auth
ENDPOINT_CUSTOM_STRUCTURES opc.tcp://localhost:4845 Custom DataTypes for codec tests
ENDPOINT_ECC_NIST (:4848) opc.tcp://localhost:4848 ECC NIST P-256/P-384 endpoints
ENDPOINT_ECC_BRAINPOOL (:4849) opc.tcp://localhost:4849 ECC Brainpool P-256/P-384 endpoints
ENDPOINT_NODE_MANAGEMENT (:24840) opc.tcp://localhost:24840 open62541 with NodeManagement

There is no env-var indirection in the test suite — start both compose stacks once and the constants do the rest.

Local setup

bash terminal — start everything
# uanetstandard-test-suite (8 servers)
git clone https://github.com/php-opcua/uanetstandard-test-suite
(cd uanetstandard-test-suite && docker compose up -d)

# extra-test-suite (open62541 for NodeManagement)
git clone https://github.com/php-opcua/extra-test-suite
(cd extra-test-suite && docker compose up -d)

Both stacks use restart: unless-stopped by default — they survive a dev-machine reboot, so you start them once and forget.

Writing tests against the suites

Reuse TestHelper::connectFor*() to short-circuit the boilerplate:

php tests/Integration/MyDeviceTest.php
use PhpOpcua\Client\Tests\Integration\Helpers\TestHelper;

it('reads a string node from the test server', function () {
    $client = TestHelper::connectForNoSecurity();   // ENDPOINT_NO_SECURITY

    $dv = $client->read('ns=2;s=Demo.Static.Scalar.String');

    expect($dv->statusCode)->toBe(0);
    expect($dv->getValue())->toBeString();

    $client->disconnect();
});

The TestHelper lives in tests/Integration/Helpers/ — copy it into your own integration suite or import it as a dev-dependency reference.

CI integration

The library's own CI does the same:

  • The integration workflow runs against both stacks on every PHP matrix leg.
  • Both stacks are consumed via composite GitHub Actions (php-opcua/[email protected], php-opcua/[email protected]) that docker compose pull + up -d and emit health checks.
  • Stack containers run with restart: "no" in CI — failures surface rather than auto-recover.

The same composite actions are public — drop them into your own GitHub Actions workflow when you want CI coverage with the same servers:

text .github/workflows/integration.yml
- uses: php-opcua/[email protected]
  with:
    profile: full   # or 'no-security' for the minimum
- uses: php-opcua/[email protected]
  with:
    services: node-management
- run: vendor/bin/pest --group=integration

Grouping

The library's integration tests are tagged ->group('integration'). The Pest convention:

bash terminal — run only integration
vendor/bin/pest --group=integration         # only integration
vendor/bin/pest --exclude-group=integration # everything else

Use the exclude form in CI for the fast unit pass; gate the integration pass on Docker availability.

Patterns

Connect once per test class.

php connect-once pattern
beforeAll(function () {
    $this->client = TestHelper::connectForSignAndEncrypt();
});

afterAll(function () {
    $this->client->disconnect();
});

it('writes and reads back', function () {
    $this->client->write('ns=2;s=Tag', 42);
    $dv = $this->client->read('ns=2;s=Tag', refresh: true);
    expect($dv->getValue())->toBe(42);
});

Each connect() costs ~30-100 ms against the test stacks; sharing the client across a test class is the difference between a 5-second and a 50-second suite.

Reset between scenarios.

If your tests mutate server state (writes, NodeManagement, trust store entries), reset deliberately at the end:

php cleanup pattern
afterEach(function () {
    $this->client->write('ns=2;s=Tag', 0);   // known baseline
    $this->client->flushCache();
});

Skip when the server is unavailable.

The library's test helper handles this for you:

php conditional skip
beforeEach(function () {
    TestHelper::skipIfEndpointUnreachable(TestHelper::ENDPOINT_NODE_MANAGEMENT);
});

Performance

Integration tests are slow relative to unit tests — each is one or more real OPC UA round-trips. On a quiet machine:

  • A connect() + disconnect() cycle: ~30-100 ms
  • A single read(): ~1-5 ms
  • A browseRecursive() over a small subtree: ~10-50 ms

A full integration suite runs in 10-30 seconds. If yours runs longer, the suspect is usually a missing connect-once pattern.

When integration is the wrong tool

  • Testing application logic that branches on the client. Use MockClient — see MockClient.
  • Testing exception handling for transport failures. The integration servers are reliable; simulating breakage means Docker-pausing them mid-test, which is messy. Stub the transport instead.
  • Testing encoder/decoder correctness. The library does this in tests/Unit/Encoding/ with hand-crafted byte fixtures — faster and more reproducible than a round-trip.