The database is empty, so the test has to build the invoice. Invoice::factory()->create(['status' => 'issued', 'issued_on' => now()]) says which columns changed. It does not say why the test needs that invoice. A state gives the reason a name, and every test shares the same columns.
Test-driven Laravel · Lesson 1
Factories that read like the domain
Video
01 Name the state the test is about
The default factory stays a normal invoice: a draft, on a team, with every required column filled. issued() is the edge the test asked for.
class Example
{
public function issued(): static
{
return $this->state(fn () => [
'status' => 'issued',
'issued_on' => now()->toDateString(),
]);
}
}$invoice = Invoice::factory()->issued()->create();
$this->actingAs($invoice->team->owner)
->post('/invoices/'.$invoice->id.'/send')
->assertRedirect();The snippet on factory states shows the same idea for a void invoice, with voided().
Leave the overrides in the test only when that one test is the reason the value exists. If a second test copies the array, it is a state.
02 Chain a second state when the test needs both
issued() sets the status and the date. forTeam() sets the team when the test already has one. Chaining reads as the invoice the test means, and the default factory still fills the columns neither state mentioned.
class Example
{
public function forTeam(Team $team): static
{
return $this->state(fn () => [
'team_id' => $team->id,
]);
}
}$invoice = Invoice::factory()
->forTeam($user->currentTeam)
->issued()
->create();A state that also creates lines, mail and a payment is doing the test’s arrange in hiding. Keep a state to the columns that define the word in its name. The test adds the lines when the lines are what that test is about.
afterCreating() is the hook for those lines, and only on the state that means “an invoice with lines”. A default factory that always creates lines makes every test pay for rows it never asserts. Name that state withLines() when a second test needs it. Until then, the test can call lines()->create() itself.
Recap
You can name issued() so a test reads as the invoice it means, and keep the default factory boring.
- A state shared by two tests belongs in the factory. A value one test needs can stay in that test.
- Chain states. Do not hide a second fact inside the first.
- A state that also sends mail is the test's arrange in hiding.
Check yourself · 3 questions
Question 1 of 3Two tests copy ['status' => 'issued', 'issued_on' => now()]. What is that array?