STALE-STATUS (OpcPlcFixture.cs:39):
- "What the fixture is": opc.tcp://localhost:50000 → opc.tcp://10.100.0.35:50000
(shared Docker host migrated 2026-04-28; fixture already defaults to 10.100.0.35)
CODE-REALITY (OpcUaClientSmokeTests.cs — 3 integration tests open real Secure Channels):
- "What it does NOT cover" §1 ("No UA Secure Channel is ever opened") was wrong
for the integration suite which does open real channels. Rewritten to scope the
no-Secure-Channel claim to the unit suite and list what the integration suite
still doesn't exercise (non-anonymous security policies, signing/encryption,
chunk assembly, keep-alive).
- "When to trust" table: added Integration (opc-plc) column; noted that real OPC UA
read + subscribe ARE covered by integration tests; write not yet exercised on wire.
NOTE on IRediscoverable: OpcUaClientDriver does NOT implement IRediscoverable
(verified: no reference in src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.OpcUaClient/).
Doc makes no such claim — no change needed for that aspect.
INLINE COMPLETENESS:
- "Key fixture / config files": added OpcPlcFixture.cs, OpcUaClientSmokeTests.cs,
and Docker/docker-compose.yml entries with correct endpoints and flags.
- Added explicit note in OpcUaClientDriver.cs entry: implements IAlarmSource +
IHistoryProvider (unique among drivers); does NOT implement IRediscoverable.
STRUCTURAL: no rows in links-report.md for this doc.
VERIFY: check_links.py — 0 rows for OpcUaClient-Test-Fixture.md.
9.1 KiB
OPC UA Client test fixture
Coverage map + gap inventory for the OPC UA Client (gateway / aggregation) driver.
TL;DR: Wire-level coverage now exists via
opc-plc — Microsoft
Industrial IoT's OPC UA PLC simulator running in Docker (task #215). Real
Secure Channel, real Session, real MonitoredItem exchange against an
independent server implementation. Unit tests still carry the exhaustive
capability matrix (cert auth / security policies / reconnect / failover /
attribute mapping). Gaps remaining: upstream-server-specific quirks
(historian aggregates, typed ConditionType events, SDK-publish-queue edge
behavior under load) — opc-plc uses the same OPCFoundation stack internally
so fully-independent-stack coverage needs open62541/open62541 as a second
image (follow-up).
What the fixture is
Integration layer (task #215):
tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.OpcUaClient.IntegrationTests/ stands up
mcr.microsoft.com/iotedge/opc-plc:2.14.10 via Docker/docker-compose.yml
on opc.tcp://10.100.0.35:50000 (the shared Docker host; override via
OPCUA_SIM_ENDPOINT). OpcPlcFixture probes the port at
collection init + skips tests with a clear message when the container's
not running (matches the Modbus/pymodbus + S7/python-snap7 skip pattern).
Docker is the launcher — no PowerShell wrapper needed because opc-plc
ships pre-containerized. Compose-file flags: --ut (unsecured transport
advertised), --aa (auto-accept client certs — opc-plc's cert trust store
resets on each spin-up), --alm (alarm simulation for IAlarmSource
follow-up coverage), --pn=50000 (port).
Unit layer:
tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.OpcUaClient.Tests/ is still the primary
coverage. Tests inject fakes through the driver's construction path; the
OPCFoundation.NetStandard Session surface is wrapped behind an interface
the tests mock.
What it actually covers
Integration (opc-plc Docker, task #215)
OpcUaClientSmokeTests.Client_connects_and_reads_StepUp_node_through_real_OPC_UA_stack— full Secure Channel + Session +ns=3;s=StepUpRead round-tripOpcUaClientSmokeTests.Client_reads_batch_of_varied_types_from_live_simulator— batch Read of UInt32 / Int32 / Boolean; assertsbool-specific Variant decoding to catch a common attribute-mapping regressionOpcUaClientSmokeTests.Client_subscribe_receives_StepUp_data_changes_from_live_server— realMonitoredItemsubscription againstns=3;s=FastUInt1(ticks every 100 ms); assertsOnDataChangefires within 3 s of subscribe
Wire-level surfaces verified: IDriver + IReadable + ISubscribable +
IHostConnectivityProbe (via the Secure Channel exchange).
Unit
The surface is broad because OpcUaClientDriver is the richest-capability
driver in the fleet (it's a gateway for another OPC UA server, so it
mirrors the full capability matrix):
OpcUaClientDriverScaffoldTests—IDriverlifecycleOpcUaClientReadWriteTests— read + write lifecycleOpcUaClientSubscribeAndProbeTests— monitored-item subscription + probe state transitionsOpcUaClientDiscoveryTests—GetEndpoints+ endpoint selectionOpcUaClientAttributeMappingTests— OPC UA node attribute → driver value mappingOpcUaClientSecurityPolicyTests—SignAndEncrypt/Sign/Nonepolicy negotiation contractOpcUaClientCertAuthTests— cert store paths, revocation-list configOpcUaClientReconnectTests— SDK reconnect hook +TransferSubscriptionsacross the disconnect boundaryOpcUaClientFailoverTests— primary → secondary session fallback per driver configOpcUaClientAlarmTests— A&E severity bucket (1–1000 → Low / Medium / High / Critical), subscribe / unsubscribe / ack contractOpcUaClientHistoryTests— historical data read + interpolation contract
Capability surfaces whose contract is verified: IDriver, ITagDiscovery,
IReadable, IWritable, ISubscribable, IHostConnectivityProbe,
IAlarmSource, IHistoryProvider.
What it does NOT cover
1. Full real-stack exchange (unit tests only)
The unit suite mocks Session.ReadAsync, Session.CreateSubscription,
Session.AddItem, etc. — no UA Secure Channel is opened. The integration
suite (OpcUaClientSmokeTests, task #215) does open a real Secure Channel
against opc-plc and exercises Read + Subscribe end-to-end. What remains
untested even in the integration suite: certificate validation under
non-anonymous security policies, signing/encryption, nonce handling, chunk
assembly, keep-alive cadence — all SDK-internal.
2. Subscription transfer across reconnect
Contract test: "after a simulated reconnect, TransferSubscriptions is
called with the right handles." Real behavior: SDK re-publishes against the
new channel and some events can be lost depending on publish-queue state.
The lossy window is not characterized.
3. Large-scale subscription stress
100+ monitored items with heterogeneous publish intervals under a single session — the shape that breaks publish-queue-size tuning in the wild — is not exercised.
4. Real historian mappings
IHistoryProvider.ReadRawAsync + ReadProcessedAsync +
ReadAtTimeAsync + ReadEventsAsync are contract-mocked. Against a real
historian (AVEVA Historian, Prosys historian, Kepware LocalHistorian) each
has specific interpolation + bad-quality-handling quirks the contract test
doesn't see.
5. Real A&E events
Alarm subscription is mocked via filtered monitored items; the actual
EventFilter select-clause behavior against a server that exposes typed
ConditionType events (non-base BaseEventType) is not verified.
6. Authentication variants
- Anonymous, UserName/Password, X509 cert tokens — each is contract-tested but not exchanged against a server that actually enforces each.
- LDAP-backed
UserName(matching this repo's server-sideLdapUserAuthenticator) requires a live LDAP round-trip; not tested.
When to trust OpcUaClient tests, when to reach for a server
| Question | Unit tests | Integration (opc-plc) | Real upstream server |
|---|---|---|---|
| "Does severity 750 bucket as High?" | yes | - | yes |
"Does the driver call TransferSubscriptions after reconnect?" |
yes | - | yes |
| "Does a real OPC UA read round-trip work?" | no | yes | yes |
| "Does a real OPC UA subscribe deliver changes?" | no | yes | yes |
| "Does write round-trip work against a live server?" | no | no (not yet exercised) | yes (required) |
| "Does event-filter-based alarm subscription return ConditionType events?" | no | no | yes (required) |
| "Does history read from AVEVA Historian return correct aggregates?" | no | no | yes (required) |
| "Does the SDK's publish queue lose notifications under load?" | no | no | yes (stress) |
Follow-up candidates
The easiest win here is to wire the client driver tests against this
repo's own server. The v2 integration test project
tests/Server/ZB.MOM.WW.OtOpcUa.OpcUaServer.IntegrationTests/DualEndpointTests.cs
(the v2 replacement for the retired v1 OpcUaServerIntegrationTests) already
stands up a real OPC UA server on a non-default port with a seeded
FakeDriver. An OpcUaClientLiveLoopbackTests that connects the client
driver to that server would give:
- Real Secure Channel negotiation
- Real Session / Subscription / MonitoredItem exchange
- Real read/write round-trip
- Real certificate validation (the integration test already sets up PKI)
It wouldn't cover upstream-server-specific quirks (AVEVA Historian, Kepware, Prosys), but it would cover 80% of the SDK surface the driver sits on top of.
Beyond that:
- Prosys OPC UA Simulation Server — free, Windows-available, scriptable.
- UaExpert Server-Side Simulator — Unified Automation's sample server; good coverage of typed ConditionType events.
- Dedicated historian integration lab — only path for historian-specific coverage.
Key fixture / config files
tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.OpcUaClient.Tests/— unit tests with mockedSessiontests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.OpcUaClient.IntegrationTests/OpcPlcFixture.cs— collection fixture; parsesOPCUA_SIM_ENDPOINT(defaultopc.tcp://10.100.0.35:50000), TCP-probes at collection init, recordsSkipReasonwhen unreachabletests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.OpcUaClient.IntegrationTests/OpcUaClientSmokeTests.cs— wire-level test suite (3[Fact]methods: read, batch read, subscribe)tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.OpcUaClient.IntegrationTests/Docker/docker-compose.yml—mcr.microsoft.com/iotedge/opc-plc:2.14.10with--ut --aa --alm --pn=50000src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.OpcUaClient/OpcUaClientDriver.cs— ctor + session-factory seam tests mock through; implementsIAlarmSource+IHistoryProvider(unique among drivers); does NOT implementIRediscoverabletests/Server/ZB.MOM.WW.OtOpcUa.OpcUaServer.IntegrationTests/DualEndpointTests.cs— the v2 dual-endpoint integration harness a future loopback client test could piggyback on (v1OpcUaServerIntegrationTests.csretired with the v1 server project)