Testing Your Configuration
Testing Your Configuration
Autoscaling configuration is code that only runs in production, under conditions you cannot reproduce by hand. A per-tenant connection limit either holds or does not, and finding out from the third party is expensive.
The package ships the fakes and assertions to settle it in a normal test.
use Cbox\LaravelQueueAutoscale\Testing\InteractsWithAutoscaling;
use Cbox\LaravelQueueAutoscale\Testing\QueueMetricsFactory;
uses(InteractsWithAutoscaling::class);
Everything below drives the real scaling engine with your real configuration. Only the stores that need Redis are substituted.
Asserting what a queue will ask for
test('the exports queue scales up when it falls behind', function () {
$this->assertWorkersDemanded(0, QueueMetricsFactory::idle('exports'));
$this->assertWorkersDemanded(
6,
QueueMetricsFactory::behind(pending: 500, oldestJobAge: 120, queue: 'exports'),
);
});
QueueMetricsFactory distinguishes three states, and the difference decides the answer:
| Meaning | |
|---|---|
idle() |
Nothing waiting. |
backlogged($pending) |
Work waiting, none of it late yet. |
behind($pending, $oldestJobAge) |
Work waiting and the SLA budget being spent. |
backlogged and behind are not interchangeable. Backlog drain contributes nothing until the SLA
budget is half spent, so a deep queue and a late queue produce very different targets — testing only
the first will convince you scaling does not work.
Asserting a cap rather than a number
For a queue whose parallelism is dictated by something downstream, the useful assertion is not that a given backlog produces a given number. It is that no backlog produces more than the limit:
test('no tenant ever gets a sixth connection', function () {
$this->assertWorkersCappedAt(5, 'scrape-tenant-42');
});
This drives the queue with backlogs from ten to a hundred thousand jobs, all of them long overdue, and fails if any of them asks for more than the cap. See Respect a Per-Tenant Connection Limit.
Testing the failure fuse
Waiting for real failures is not an option, so seed them:
test('a failing payment provider stops the queue scaling up', function () {
$behind = QueueMetricsFactory::behind(1000, oldestJobAge: 300, queue: 'payments');
$this->fakeFailureWindows();
expect($this->workersDemandedFor($behind))->toBeGreaterThan(0);
$this->tripFuseFor('payments');
expect($this->workersDemandedFor($behind))->toBe(0);
});
fakeFailureWindows() returns the store, so a spec can also seed a backdated state change and expire
a cooldown without sleeping.
Testing cluster behaviour
test('the leader distributes work across hosts', function () {
$cluster = $this->fakeCluster()
->withManager($stateForWebOne)
->withManager($stateForWebTwo)
->withLeader('web-01');
// …run your listener or command…
expect($cluster->publishedRecommendations())->toHaveCount(2);
});
FakeClusterStore keeps everything in memory. It hands leadership to whoever asks first and holds it
there, which is enough to test what a manager does with cluster state — and deliberately not enough
to test whether election itself is sound. The real store needs Redis for that: leader election
depends on atomic compare-and-set and a reliable TTL, and a fake that pretends otherwise would hand
two managers the same lease and pass.
Testing SLA breach
Breach is judged on the pickup-time percentile when the SLA window has enough samples, and on the
oldest job's age otherwise. fakePickupTimes() swaps the pickup store for an in-memory one you can
seed, so a spec can put a measured p95 in front of the strategy — the only signal a driver like
Amazon SQS has, since it cannot report the oldest job's age:
test('the orders queue on SQS breaches when pickups are slow', function () {
$pickups = $this->fakePickupTimes();
$queue = QueueMetricsFactory::onSqs(200, 'orders'); // oldest job age 0, as SQS reports it
$this->assertWithinSla($queue); // nothing measured: judged on age 0
$pickups->seedRepeated(20, 45.0, 'sqs', 'orders'); // 20 pickups that waited 45s
$this->assertBreachesSla($queue);
expect($this->slaSignalFor($queue)->describe())->toBe('p95 pickup=45.0s');
});
| Helper | What it does |
|---|---|
fakePickupTimes() |
Binds and returns an InMemoryPickupTimeStore, and re-resolves the strategy, engine and manager so they use it |
slaSignalFor($metrics) |
The SlaSignal the queue is judged on — through the strategy, or measured directly for a pinned queue |
assertBreachesSla($metrics) / assertWithinSla($metrics) |
Assert that signal is, or is not, at or over sla.target_seconds |
QueueMetricsFactory::onSqs($pending) |
A snapshot on the sqs connection with an oldest job age of 0 |
InMemoryPickupTimeStore stamps samples with Carbon's clock, so Carbon::setTestNow() ages them
out of sla.window_seconds the way real time ages them out of Redis — that is how a spec drives
recovery. seed([...]), seedRepeated($count, $seconds) and clear() take an optional connection
and queue, defaulting to redis / default.
These assert the instantaneous signal. Whether SlaBreached fires also depends on the previous
state — it fires on the transition, and an open breach is held through an inconclusive reading —
which the manager tracks across cycles. To assert the events themselves, combine Event::fake()
with your own scheduling of the manager, or listen in an integration test.
What is not faked
Host CPU and memory limits. workersDemandedFor() returns cluster-wide demand — the strategy's
answer bounded by the queue's own minimum and maximum and by the fuse. Host resource ceilings are
left out on purpose, because they depend on the machine running the test, and a configuration that
asserted differently on a laptop and in CI would be worse than no assertion. To test the ceiling
itself, use CapacityCalculator directly.
Your queue driver. These helpers reason about metrics, not about queues. To check that jobs actually reach a queue and that depth reads back, write an integration test — see Requirements for the SQS setup the package uses for its own.