Writing tests for a production Laravel application using Inertia in PestPHP

Taking the right testing approach

As I began my testing journey, I wanted to remain nimble. I feel very productive with the current offerings from the Laravel framework. The last thing I wanted was to spend a large effort to write beautiful tests, but then be held back by failing tests when I changed something in the future. I wanted a broad coverage approach, that, in the end, would help me ship faster with confidence, but also not be a burden to update.

There are plenty of debates about approaches to developing applications1: TDD (Test Driven Development), DDD (Domain Driven Development), BDD (Behavior Driven Development). I will not prescribe one solution as it truly depends on your needs, your product, your team size, tech stack, and so much more...

Here are my guiding principles:

  1. Write tests for happy paths, starting with core logic and most used product flows
  2. Keep tests at the right layer/level: unit first, and only go deep (end-to-end) when required
  3. Run your tests on CI before deploying, so you get the maximum value out of your efforts

Available tools for Laravel

We have various testing frameworks and tools, but the ones I would recommend that are currently state of the art:

Ok so where do I start?

First off, let's tackle writing tests for our core business logic. I started off stepping back and looking at my application:

  • Importance: What product features do my users use most? Or expect to use most?
    • Think: For the average user, what is the most used product flows and core features?
  • Complexity: Where does high complexity live in my application?
    • Think: Places I have lower confidence? Things that I test manually often? Places with lots of comments?
  • Stability: If things go really wrong in a feature/flow, how much trouble would I be in?
    • Think: Is there any feature that would take my site down completely?

Taking the intersection of those focus areas, you can land on a list resembling product features or core logic that is: important, complex, and keeps stability high.

Structuring Laravel tests in PestPHP

I like to follow the format of creating test files and folders that map to my actual controller logic. This makes it easy to find things now and in the future.

The convention is as follows:

  • App/Http/Controllers/UserController.php --> tests/Features/Http/Controllers/UserControllerTest.php

Note: If you started your application with a starter kit like Laravel Jetstream, you will likely already have tests scaffolded for you that exist in the: tests/Features/Jetstream folder.

Writing Laravel tests for controllers

Usually I structure my controllers to be CRUD and try to follow Laravel conventions as much as I can, which is a great strategy to have a maintainable app throughout the years. My controllers usually have these methods:

  • index(): show a list of MyModel
  • show(MyModel $model): show a specific instance of MyModel
  • store(MyModel $model): persist a new instance of MyModel
  • update(MyModel $model): update an existing instance of MyModel
  • delete(MyModel $model): delete an existing instance of MyModel

All methods are important, but I like to start out with index() to get momentum, in which case I create something like this:

UserContactControllerTest.php

Context: This test is using PestPHP, Inertia, and laravel-data (from the amazing folks at Spatie).

  1. Create test user via Laravel eloquent factories and login via actingAs
  2. Create controller specific test data, with more factories
  3. Get response via get and action methods, via Laravel pest plugin
  4. Assert response is an Inertia response with a specific page Component and specific props
1use App\Http\Controllers\UserContactController; ...
2use App\Models\User;
3use App\Models\UserContact;
4use Inertia\Testing\AssertableInertia;
5use function Pest\Laravel\get;
6use function Pest\Laravel\actingAs;
7 
8beforeEach(function () {
9 $this->user = User::factory()->createOne();
10 actingAs($this->user);
11});
12 
13test('can show list of contacts', function () {
14 $contacts = UserContact::factory()
15 ->for($this->user)
16 ->count(10)
17 ->create();
18 
19 $response = get(action([UserContactController::class, 'index']));
20 
21 $response->assertInertia(function (AssertableInertia $page) {
22 $page
23 ->component('Contacts/ContactsPage')
24 ->has('viewData.paginatedUserContacts', function (AssertableInertia $page) {
25 ...
26 });
27 });
28})

Benefits of this feature test

You are testing the happy path, given scaffolded test data, covering:

  • Model logic: your controller should fetch the models created from factories and pass into Inertia page
  • Authorization logic: assuming your controller has a Form request with attached authorization
  • Inertia logic: ensuring this page is the right page component, and this component has the right props view data

There is a whole lot more to test here, if you'd like, but this is a good starting point.

Next steps

Overall there is a lot to go into on how to test things the right way. This test is an introduction to testing Laravel applications, but will go a long way. There are more things to cover to make sure you are shipping quality with guardrails in place:

  • Testing that queues, mail, events are working appropriately using facade fakes
  • Testing that your SPA frontend (Inertia for example) has no Javascript errors
    • Think: The test we have above is only testing the server side, but there can be an issue on client side that will block rendering the content to the end user
  • Testing model CRUD operations: insert, update, delete. These tests will focus on passing in input to API/route, then asserting database has or doesn't have certain elements
  • Running your tests in CI! Github Actions is great for this, but it's not always straightforward to get set up and a bit of a pain to get right

Final thoughts

As you become more familiar with writing and running tests, it becomes an important feedback loop and a great source of pleasure when you make large scale changes, run your test suite (which you know covers your changes) and all things are green!

Although 100% test coverage shouldn't be your goal, my advice would be test the things that you think are important, and don't think too hard about it after that. Stay tuned for more real-world testing advice!