Skip to content

Tier 2b · Chapter 7: Testing in Laravel

PHP/Laravel Track · ~30 min read + exercise

A note on sources: This chapter draws on general testing practice (the test pyramid, test doubles — the same principles as the JS/TS track's testing chapter) and Laravel's testing documentation. The course's book set doesn't cover testing in depth; treat the testing-strategy principles as well-established industry practice and the Laravel/Pest specifics as current-framework examples.


Why this chapter exists

This chapter closes the PHP/Laravel track the same way testing closed the JS/TS track: it's where the boundaries you've built throughout the track pay off as verification, and where the same portfolio-balancing trade-off applies. The testing strategy is framework-independent — the pyramid, the levels, testability as a structural property, the trade-off between confidence and speed — so this chapter doesn't re-derive it (the JS/TS testing chapter has the full treatment). Instead it focuses on what's specifically valuable and specifically tempting in Laravel: the framework's excellent testing ergonomics, which make both good and lazy testing easy, and the way Laravel's layers (form requests, services, policies, repositories) map onto test levels.

The throughline carries from the JS/TS track unchanged because it's a property of testing, not of stacks: a good test suite is a balanced portfolio across levels, weighted toward where your real bugs live, and testability is a structural property your boundaries created. Laravel's specific contribution is making feature tests (full-request integration tests) so easy that teams sometimes over-rely on them — the "ice cream cone" anti-pattern from the JS/TS chapter — so the calibration judgment matters here in a particular way.

What this chapter covers

  • The pyramid in Laravel — how the framework's test types map to the levels, and Laravel's particular temptation.
  • Unit tests and the boundaries that enable them — testing domain logic without the framework.
  • Feature tests — Laravel's integration tests, what they cover well, and the over-reliance trap.
  • Architectural boundary tests — fitness functions for PHP/Laravel structure.

The pyramid in Laravel

The test pyramid from the JS/TS testing chapter applies unchanged — many fast unit tests, fewer integration tests, very few end-to-end — and so does the core lesson that the balance is an architectural trade-off between confidence, speed, and maintenance cost, weighted toward where your real bugs occur. Laravel's test types map onto the levels: unit tests exercise a class in isolation; feature tests exercise a full request through the framework (routing, middleware, validation, controller, database) — which makes them integration-to-end-to-end in scope; and you can build true end-to-end tests (browser-level, via Laravel Dusk) at the top.

Laravel's particular temptation is worth naming because it's so easy to fall into: the framework makes feature tests so pleasant that teams write almost exclusively feature tests — exercising everything through full HTTP requests against a real (test) database — and end up with the "ice cream cone" shape the JS/TS chapter warned against. Feature tests are valuable (they catch the integration bugs that, as that chapter noted, are where I/O-heavy apps actually break), but a suite that's only feature tests is slow, and slow suites get run less and trusted less. The calibration judgment: lean on feature tests for the integration coverage they genuinely provide (Laravel apps, like Node services, are I/O-heavy, so the "testing trophy" weighting toward integration is often right), but keep the fast unit tests underneath for the domain logic, so the suite stays fast enough to run constantly. Don't let Laravel's feature-test ergonomics seduce you into an all-integration suite.


Unit tests and the boundaries that enable them

The JS/TS chapter's central insight holds identically in Laravel: testability is a structural property, and the boundaries you built earlier in this track are what make fast unit tests possible. The fat-controller chapter's whole argument was that business logic welded to the framework can't be tested without the framework; extracting it into services/actions and inverting dependencies behind repository interfaces (via the service container) is exactly what lets you test the logic in isolation.

The payoff, concretely: the "max 5 open tickets" rule, once it lives in a CreateTicket action depending on a TicketRepository interface rather than in a controller calling Eloquent directly, can be tested with a fake repository and no database — fast, focused, pinpointing a failure to the rule itself:

php
// Fast unit test — no framework, no database, just the rule.
it('rejects a sixth open ticket', function () {
    $tickets = new InMemoryTicketRepository(/* 5 open tickets */);
    $action = new CreateTicket($tickets);

    expect(fn () => $action->execute($dto))
        ->toThrow(TooManyOpenTicketsException::class);
});

This connects every earlier chapter of the track: the value objects and rich entities (chapters 1, 3) give you logic worth unit-testing, the layering and dependency inversion (chapter 2) make it testable without the framework, and the repository boundary lets a fake stand in for the database. Code that's hard to unit-test in Laravel is almost always code that skipped those boundaries — logic still fused to the controller or reaching into Eloquent directly. The difficulty of testing is, again, a design signal: it's telling you the boundary isn't there. (The test-double vocabulary — stub, mock, fake — and the caution against over-mocking are the same as the JS/TS testing chapter; refer there for the full treatment.)

Laravel runs tests on PHPUnit (with Pest as a popular expressive layer on top); the mechanics differ from the JS/TS tools but the discipline is identical — lightest double that gives confidence, test behavior not internal call sequences, keep unit tests fast and free of the framework. (Flagged: PHPUnit/Pest are the Laravel-ecosystem tools; the testing principles are general industry practice from the JS/TS chapter.)


Feature tests

Laravel's feature tests exercise a full request through the framework — they hit a route, run the real middleware, validation, controller, and database (a test database), and assert on the response and the resulting state. They are integration tests in the JS/TS chapter's sense, and they're where Laravel testing shines, because they catch exactly the seam bugs that unit tests stub away: the validation that's wrong, the authorization that's missing (the security chapter's insecure-direct-object-reference — a feature test asserting another user gets a 403 catches it), the query that misbehaves against the real database, the full request flow that breaks at an integration the units couldn't see.

php
// Feature test — the full request through the framework against a test database.
it('forbids viewing another customer\'s ticket', function () {
    $ticket = Ticket::factory()->create();      // belongs to some other customer
    $this->actingAs(User::factory()->create())  // a different, authenticated user
        ->getJson("/api/tickets/{$ticket->id}")
        ->assertForbidden();                     // 403 — authorization works
});

The guidance, carrying the JS/TS integration-testing lessons into Laravel:

Test against a real (test) database, not mocks of it. The point of a feature test is to catch what only real integration reveals — actual query behavior, constraints, the full middleware stack. Laravel's testing tools make this easy (a transaction-wrapped or refreshed test database per test), and mocking the database in a feature test would defeat its purpose, testing the mock instead of the integration.

Cover the seams that matter — the request flows where bugs concentrate: validation, authorization (every policy from the security chapter deserves a feature test proving denial works), the critical request-to-database paths. These are the boundaries the track built, verified end-to-end.

Keep them isolated and fast enough to run constantly. Flaky feature tests that share state or depend on order erode trust exactly as in the JS/TS chapter — Laravel's database-refresh/transaction-rollback-per-test tooling keeps them isolated, and the discipline is not letting the feature-test suite grow so large and slow that it stops being run. This is the calibration point again: enough feature tests for real seam coverage, not so many that they become the slow suite nobody trusts.


Architectural boundary tests

The final connection back to Foundations Chapter 6: tests can verify not just behavior but architecture — the fitness-function idea, in PHP/Laravel form. The module and layering boundaries this track built (the domain not depending on the framework, modules not reaching into each other's internals) are conventions that erode without enforcement, and an automated architectural test catches the erosion in CI.

In the PHP ecosystem, the tools are Deptrac (and PHPat/pest-plugin-arch for architecture assertions within the test suite), which let you declare layer and dependency rules and fail the build when code violates them — exactly the structural fitness function from Foundations Chapter 6, and the direct analog of the JS/TS track's dependency-cruiser boundary check:

php
// An architecture test (Pest's arch plugin) enforcing the dependency direction
// from the layering chapter: the domain must not depend on the framework.
arch('domain does not depend on the framework')
    ->expect('App\Domain')
    ->not->toUse('Illuminate');

This enforces, automatically and continuously, the architecture the whole track has been building: the domain stays framework-independent (chapter 2's dependency direction), modules don't reach past their boundaries, the layers respect their direction. Without it, those boundaries are conventions that decay one convenient violation at a time (Foundations Chapter 6's drift); with it, a violation gets a red build the moment it's introduced. The architecture you designed becomes the architecture the build enforces — closing the loop from the governance chapter, through this track's boundaries, to an automated check that keeps them real. (Flagged: Deptrac and the Pest arch plugin are PHP-ecosystem tools; the fitness-function concept is from Foundations Chapter 6 and the source books.)

One newer wrinkle, the same as the JS/TS track: if a feature calls an LLM (the capstone's AI axis), you can't assert an exact-match result — the same input can return different output. Verification shifts from assertions to evals: a curated set of inputs with acceptance criteria and a tracked threshold, rather than a single pass/fail. The pyramid is unchanged; only the nondeterministic step is verified differently. (Flagged: evals and LLM-as-judge are current-ecosystem practice, not from the source books.)

That's testing in Laravel: the same portfolio-balanced strategy as the JS/TS track (resist the all-feature-test temptation), fast unit tests enabled by the boundaries the track built, feature tests covering the real integration seams (including the security authorizations), and architectural boundary tests that turn the track's structural lessons into a fitness function the build enforces. The strategy is framework-independent; Laravel supplies excellent ergonomics and one specific temptation to calibrate against.


Practice

Budget about ninety minutes.

Exercise 1: Diagnose your suite's shape (20 min)

For a Laravel app you work on, count tests by type — unit vs. feature (vs. Dusk). Is it an ice-cream-cone (almost all feature tests)? Then ask where your real production bugs come from and whether the distribution matches. Laravel's feature-test ergonomics make the ice-cream-cone especially common, so look honestly.

Exercise 2: Unit-test a rule without the framework (35 min)

Find a business rule currently only covered (if at all) by a feature test that runs the whole framework. If the logic is testable in isolation, write a fast unit test with a fake repository; if it isn't testable in isolation, that's the signal from chapter 2 — the logic is still fused to the framework, and the fix is extracting it. Either way, note what the testability told you about the structure.

Exercise 3: Add an architecture test (35 min)

Add one architectural boundary test (Pest arch plugin or Deptrac) enforcing a rule from this track — the domain not depending on Illuminate, or a module not importing another's internals. Run it, and if it fails, you've found real drift; if it passes, you've locked the boundary against future drift. Note how this changes enforcement versus relying on code review.

Deliverable

A short testing assessment for your Laravel app: its pyramid shape vs. where bugs occur (and whether it's ice-cream-cone), one rule moved to a fast framework-free unit test (or identified as needing extraction first), one critical seam covered by a feature test (ideally a security authorization), and one architectural boundary test enforcing a track principle — each with the bug class it catches.

Check yourself

  • Explain how Laravel's test types map to the pyramid levels and why the framework tempts the ice-cream-cone shape.
  • Explain why testability is structural and how the track's boundaries (services, dependency inversion, repositories) enable fast unit tests.
  • Explain what feature tests cover well (the seams, including authorization) and why they should run against a real test database, not mocks.
  • Explain how an architectural boundary test (Deptrac / Pest arch) is a fitness function enforcing the track's structure.

What a strong answer demonstrates: you treat the suite as a balanced portfolio weighted toward where your real bugs live — and can name Laravel's specific temptation (feature-test ergonomics seducing you into an all-integration "ice cream cone") as a thing to calibrate against, not follow. The pivotal insight to land is that testability is structural: the fast framework-free unit test is possible only because the earlier chapters' boundaries (services/actions, dependency inversion, repositories) exist, so hard-to-test code is a design signal, and an architectural boundary test turns the track's structure into a fitness function the build enforces.

Going deeper (optional)

(Testing strategy is general practice — see the JS/TS track's testing chapter for the full treatment of pyramid, test doubles, and contract testing.) The Laravel testing documentation and the Pest documentation are the sources for the framework mechanics; Deptrac's documentation for architectural rules. Foundations Chapter 6 (fitness functions) is the conceptual source for the architectural boundary tests.

  • Primary source: Kent Beck, Test-Driven Development: By Example (Addison-Wesley, 2002) — the seminal text on writing the test first and letting the difficulty of testing drive the design, which is the deep origin of this chapter's "hard-to-test code signals a missing boundary."
  • Primary source: Mike Cohn, Succeeding with Agile: Software Development Using Scrum (Addison-Wesley, 2009) — the book that introduced and named the test pyramid (many fast unit tests, fewer service/integration, very few UI), the model this chapter calibrates Laravel's feature-test ergonomics against to avoid the "ice cream cone."

Key takeaways

  • The testing strategy is framework-independent (the JS/TS chapter's pyramid, levels, and test-double discipline apply unchanged) — a balanced portfolio weighted to where bugs live.
  • Testability is structural: the track's boundaries (services/actions, dependency inversion, repositories) are what make fast, framework-free unit tests possible; hard-to-test code signals a missing boundary.
  • Laravel's feature tests catch real integration-seam bugs (validation, authorization, queries) against a test database — but the framework's ergonomics tempt an all-feature-test ice-cream-cone, which must be calibrated against.
  • Architectural boundary tests (Deptrac, Pest arch plugin) are fitness functions (Foundations Chapter 6) that enforce the track's structure — the domain's framework-independence, module boundaries — automatically in CI.

This completes the PHP/Laravel track. You've taken the foundational architecture and made it concrete in modern PHP and Laravel: typed foundations and value objects, layered boundaries with inward dependencies, calibrated domain modeling, data consistency, async patterns, security, and testing. With both stack tracks done, the synthesis and capstone bring everything together — building one real feature two different ways and reasoning across the whole course.