Pest setup
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.
The package ships first-class Pest support. The conventions here mirror Laravel's first-party testing patterns — nothing exotic.
Disable the daemon
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.
Three test layers
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.
Unit tests — mock everything
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.
Feature tests — mock the facade
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.
Integration tests — real daemon, real server
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.
Useful Pest hooks
Disable the OPC UA cache in tests
beforeEach(function () {
Cache::flush(); // clear OPC UA's metadata cache
});
Fix the time
For tests that assert on timestamps:
beforeEach(function () {
$this->travelTo('2026-05-15 10:00:00');
});
Avoid the event leak
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);
});
Queue side-effects
use Illuminate\Support\Facades\Queue;
beforeEach(function () {
Queue::fake();
});
it('dispatches a job', function () {
// ...
Queue::assertPushed(SamplePlc::class);
});
Composing the three layers — tests/Pest.php
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.
Test data factories
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));
Running
# 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"
CI matrix
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.
Test coverage targets
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).
Where to read next
- Mocking the facade — Mockery-based facade mocks.
- Using MockClient — opcua-client's testing fake.
- Integration tests — real-server testing.