| eyebrow | Docs · Testing | |||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| lede | Pest harness for OPC UA-aware Laravel tests. Disabling the daemon for unit tests, the standard fixtures, and the three test layers most apps converge on. | |||||||||||||||
| see_also |
|
|||||||||||||||
| prev |
|
|||||||||||||||
| next |
|
The package ships first-class Pest support. The conventions here mirror Laravel's first-party testing patterns — nothing exotic.
The single most important rule: don't hit the daemon in unit
or feature tests. Set this in tests/Pest.php:
uses(Tests\TestCase::class)->in('Feature', 'Unit');
beforeEach(function () {
config([
'opcua.session_manager.enabled' => false,
]);
});With managed mode off, the package falls through to direct connections — and you'll mock those out per test.
Most Laravel apps with OPC UA settle on three test layers:
| Layer | Tests | Touches OPC UA? |
|---|---|---|
| Unit | Your business logic | No (mock everything) |
| Feature | HTTP endpoints, jobs, listeners | No (mock the facade) |
| Integration | End-to-end against a real test server / fake daemon | Yes |
Unit and Feature run on every push. Integration runs nightly or on PR-merge.
use PhpOpcua\Client\Testing\MockClient;
use PhpOpcua\Client\Types\DataValue;
it('persists a good reading', function () {
$client = MockClient::create();
$client->onRead('ns=2;s=Speed', fn() => DataValue::ofDouble(75.0));
$service = new TagReadingService($client);
$service->capture('ns=2;s=Speed');
expect(PlcReading::count())->toBe(1);
expect(PlcReading::first()->value)->toBe(75.0);
});MockClient implements the same OpcUaClientInterface as the
real package — see Using MockClient.
use PhpOpcua\LaravelOpcua\Facades\Opcua;
use PhpOpcua\Client\Types\DataValue;
it('returns the current speed', function () {
Opcua::shouldReceive('read')
->with('ns=2;s=Speed')
->andReturn(DataValue::ofDouble(75.0));
$response = $this->get('/tags/ns=2;s=Speed/latest');
$response->assertOk();
$response->assertJson(['value' => 75.0]);
});Mockery-based mocking, just like any other facade. See Mocking the facade.
use PhpOpcua\LaravelOpcua\Facades\Opcua;
uses(Tests\IntegrationTestCase::class)->in('Integration');
beforeEach(function () {
// The IntegrationTestCase boots a daemon + opens a connection to
// a known test OPC UA server.
});
it('reads from the test server', function () {
$dv = Opcua::read('i=2256'); // Server_ServerStatus_State
expect($dv->statusCode)->toBe(0);
expect($dv->value)->toBe(0); // 0 = Running
});IntegrationTestCase is yours to define — it boots the daemon
as a fixture and points the test app at it. See Integration
tests.
beforeEach(function () {
Cache::flush(); // clear OPC UA's metadata cache
});For tests that assert on timestamps:
beforeEach(function () {
$this->travelTo('2026-05-15 10:00:00');
});Listeners on DataChangeReceived will fire in tests if the event
is dispatched. To avoid side-effects:
use Illuminate\Support\Facades\Event;
use PhpOpcua\Client\Event\DataChangeReceived;
beforeEach(function () {
Event::fake([DataChangeReceived::class]);
});…then assert dispatches without running listeners:
it('dispatches a data change event on subscribe', function () {
// ... trigger the code under test
Event::assertDispatched(DataChangeReceived::class);
});use Illuminate\Support\Facades\Queue;
beforeEach(function () {
Queue::fake();
});
it('dispatches a job', function () {
// ...
Queue::assertPushed(SamplePlc::class);
});A realistic Pest.php:
<?php
uses(Tests\TestCase::class)->in('Feature', 'Unit');
uses(Tests\IntegrationTestCase::class)->in('Integration');
beforeEach(function () {
if (! $this instanceof Tests\IntegrationTestCase) {
// Unit / Feature — fully mock
config([
'opcua.session_manager.enabled' => false,
]);
}
});tests/TestCase.php extends Tests\CreatesApplication;
IntegrationTestCase adds daemon-startup logic.
For tests that need DataValue instances, a helper:
function dv(mixed $value, int $status = 0): DataValue
{
return new DataValue(
value: $value,
statusCode: $status,
sourceTimestamp: new DateTimeImmutable(),
);
}In a test:
$client->onRead('ns=2;s=Speed', fn() => dv(75.0));
$client->onRead('ns=2;s=Bad', fn() => dv(0.0, status: 0x80000000));# All except integration
vendor/bin/pest --exclude=Integration
# Integration only
vendor/bin/pest tests/Integration
# Single test
vendor/bin/pest --filter="returns the current speed"jobs:
unit-and-feature:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: shivammathur/setup-php@v2
with: { php-version: 8.4 }
- run: composer install
- run: vendor/bin/pest --exclude=Integration
integration:
runs-on: ubuntu-latest
services:
opcua:
image: ghcr.io/php-opcua/uanetstandard-test-suite:latest
ports: ['4840:4840']
steps:
- uses: actions/checkout@v4
- uses: shivammathur/setup-php@v2
with: { php-version: 8.4 }
- run: composer install
- run: vendor/bin/pest tests/Integration
Unit / Feature on every push; Integration on a separate matrix leg with the test server as a sidecar.
The package aims for 99.5% coverage. For your application:
| Test target | Reasonable coverage |
|---|---|
| OPC UA-touching services (read/write logic) | High — 90%+ |
| Event listeners | High — 90%+ |
| OPC UA-touching controllers | Medium — 80%+ |
| Trust-store / cert rotation tooling | Medium — manual rotation works too |
Don't aim for 100% — there are always cases that are disproportionate to test (real-cert handshake failures, in particular).
- Mocking the facade — Mockery-based facade mocks.
- Using MockClient — opcua-client's testing fake.
- Integration tests — real-server testing.