Tests get hard to read when every arrangement is Invoice::factory()->create(['status' => 'void', 'voided_at' => now()]). The array says what changed, not why the test needs that invoice.
A named state says why. The test reads Invoice::factory()->voided()->create(), and the column values live in one method that every test shares. The method is voided(). void is a return type, so it stays off the method name.
Keep the default factory valid and boring: an open invoice, on a team, with every required column filled. Put the edge cases in states.
01 What it does
States return a new factory, so they compose. A void invoice with two lines is still one line, and the test name can match it.
// tests/Feature/InvoicePolicyTest.php
use App\Models\Invoice;
use App\Models\InvoiceLine;
$invoice = Invoice::factory()
->voided()
->has(InvoiceLine::factory()->count(2))
->create();
$this->actingAs($invoice->team->owner)
->put(route('invoices.update', $invoice), ['number' => 'INV-2'])
->assertForbidden();has() builds the related rows. If the interesting part is the parent, use for() on the child: InvoiceLine::factory()->for($invoice).
A state can take a parameter. Default it so the common call stays short.
// database/factories/InvoiceFactory.php
use App\Models\InvoiceLine;
class InvoiceFactory
{
public function withLines(int $count = 1): static
{
return $this->has(InvoiceLine::factory()->count($count));
}
}02 Gotchas
- A state should only arrange data. Reaching a payment gateway from the factory makes every test that uses the state slow and brittle.
- Chain a second state when a void invoice also needs a credit note. Do not hide the second fact inside
voided(). afterCreating()is the right hook when a row has to exist before a follow-up write, such as a pivot. Keep expectations out of it. The test asserts.- The default factory must satisfy the database. A state that unsets a required column fails in every test that uses it, with an error that points at the insert rather than the state.
03 Related
Factory states is the lesson. Authorisation is where a void invoice is the case under test. Policies vs gates is the rule that refuses the update.