Skip to main content
Laravel, shipping fast.

With sync as the queue connection, a dispatched job runs at once inside the test. That is right for most tests: “delete a license” should end with the provider having been asked to delete it.

Sometimes the thing under test is the dispatch itself. The delete endpoint promises a 202 and a queued job. Whether the job then works is a different test.

it('queues the deletion and answers 202', function () {
    Queue::fake();

    $consumer = actingAsConsumer('licenses:write');

    $this->deleteJson('/api/licenses/lic_1')
        ->assertAccepted();

    Queue::assertPushed(
        DeleteLicense::class,
        fn (DeleteLicense $job) => $job->key === 'lic_1'
            && $job->consumer->is($consumer),
    );
});

Queue::fake() catches jobs and runs none of them. The closure checks what the job was given. The second condition is Chapter 10’s lesson as an assertion: the job carries the consumer who asked. Remove the argument from the controller and this test fails, which is exactly the regression that would otherwise show up as one partner’s license being deleted with another’s credentials.

The other half runs the job directly and asserts what it does:

it('deletes the license at the provider', function () {
    Http::fake(['*' => Http::response(status: 204)]);

    $consumer = Consumer::factory()
        ->forPartner('tok')
        ->create();

    (new DeleteLicense('lic_1', $consumer))->handle();

    Http::assertSent(
        fn ($request) => $request->method() === 'DELETE',
    );
});

Between them they cover the feature, and neither depends on a worker. Events and notifications fake the same way, with Event::fake() and Notification::fake(). Pass the class you care about, as in Event::fake([LicenseIssued::class]), and only that one is faked. Fake everything and you also silence the model events your factories rely on. (Laravel documentation: Events › Testing and Notifications › Testing.)

One Test, Many Inputs

Validation rules are where a boundary is defined, and a boundary has many edges. Writing one test per bad input is tedious enough that nobody does it. Pest’s datasets make it one test:

it('rejects an invalid license', function (
    array $body,
    string $field,
) {
    actingAsConsumer('licenses:write');

    $this->postJson('/api/licenses', $body)
        ->assertUnprocessable()
        ->assertJsonValidationErrors([$field]);
})->with([
    'no name' => [['domain' => 'a.test'], 'name'],
    'no domain' => [['name' => 'Acme'], 'domain'],
    'name too long' => [
        ['name' => str_repeat('a', 101), 'domain' => 'a.t'],
        'name',
    ],
    'domain is an array' => [
        ['name' => 'Acme', 'domain' => ['a.test']],
        'domain',
    ],
]);

Each entry runs as its own test, with its label in the output, so a failure says which edge broke. Adding a rule to the request means adding a line here.

The entries worth having are the ones at the limits: exactly at the maximum and one past it, the wrong type, the missing key. Those are the inputs a real consumer sends by accident and an attacker sends on purpose. The last entry is not hypothetical: an array where a string is expected is how a careless prepareForValidation() turns a 422 into a 500, which is why the prepareForValidation() in Chapter 2 checks the type first.

The audio could not be loaded. Try again in a moment.