Test Fixtures & setUp() Best Practices
Also Known As
TL;DR
Explanation
PHPUnit's setUp() runs before each test method — use it for common object creation but avoid coupling tests through shared mutable state. Prefer object mother classes (UserMother::typical(), UserMother::admin()) or factory functions that return fully configured objects with sensible defaults — tests override only the properties relevant to that specific assertion. This follows the Arrange-Act-Assert pattern cleanly. For database tests, use transactions rolled back in tearDown() (DatabaseTransactions trait in Laravel) rather than re-seeding — order-of-magnitude faster. Avoid static state in fixtures. Keep setUpBeforeClass() (runs once per class) for truly expensive shared resources (database schema creation, container boot) while keeping instance-level setUp() lightweight.
Common Misconception
Why It Matters
Common Mistakes
- Tests that depend on execution order — each test must set up and tear down its own state.
- Fixtures shared via static properties — mutations persist across tests.
- Over-populated fixtures — tests that need one user should not load 50 rows of seed data.
- Not using database transactions with rollback for database tests — each test gets a clean database state.
Code Examples
// Shared static fixture — order-dependent tests:
class UserTest extends TestCase {
private static User $user;
public static function setUpBeforeClass(): void {
self::$user = User::create(['email' => 'alice@example.com']);
}
public function testDeactivate(): void {
self::$user->deactivate(); // Mutates shared fixture
}
public function testEmail(): void {
// Fails if testDeactivate ran first — user is now deactivated
}
}
// Object Mother pattern
class UserMother {
public static function typical(): User {
return new User(id: 1, email: 'user@test.com', role: Role::User);
}
public static function admin(): User {
return new User(id: 2, email: 'admin@test.com', role: Role::Admin);
}
}