Easier test coverage for complex API resources

And the tests are fast too

Joel Clermont
Joel Clermont
2024-04-18

For the types of applications we typically work on, we most often have more feature tests in our suite than unit tests. These feature tests center around a simulated HTTP request coming in to the application and generating a response through the normal process of middleware, controllers, and so on.

But what do you do if you have an API endpoint which can return a fairly complex resource, which has lots of different fields, relationships, and other conditional data to test? Making each of these variations its own feature test is possible, but it can be a little cumbersome and slower than I'd like.

Instead, we often have a lighter-weight test we call an integration test that doesn't involve a full HTTP request through the Laravel app, but instead let's us test something a bit more in isolation.

If you're curious how this differs from a unit test, in our definition a unit test can't hit the database, so this rules out a lot of lighter-weight tests which still need database access.

Here's the structure of these integration-style resource tests:

public function testResourceMissingOptionalProperty(): void
{
    // simplified test setup - creating Eloquent models, etc
    $model = MyModel::factory()->create();

    // create a resource to make assertions against
    $resource = new MyResource($model)->resolve();

    // make assertions about the resource
    self::assertSame([
        // test every single expected fields and value
    ], $resource);
}

Let's walk through each of our three main sections.

First, we arrange all our test data. In a real-world test, you'll probably create a few models, set specific values in the factory, and maybe set up some relationships.

Next, we create the resource we want to test, passing in the data needed by the resource constructor. Often, this is an Eloquent model. But notice how we're calling resolve() instead of toArray(). This is an important detail!

If we call toArray() and we have conditional properties inside our resource, those properties will be included with a MissingValue object as the value.

Instead, calling resolve() has Laravel fully generate the resource, removing those conditional attributes, just like will happen when the resource is resolved and returned in a normal HTTP response.

Finally, we make our assertions. By using the strict assertSame() method, we can ensure that all the fields in our resource match exactly what we expect. If an extra field is included in the resource, our test will fail, which is what we want.

With this lighter-weight integration test, we can create as many test variations as we need to properly cover all the conditional paths through our resource. This gives us a lot more confidence without slowing down our test suite unnecessarily.

Hope this helps,

Joel

P.S. Testing is a topic we talk about quite a bit in the Mastering Laravel Slack community. Join us, and level up your testing skills!

Toss a coin in the jar if you found this helpful.
Want a tip like this in your inbox every weekday? Sign up below 👇🏼

Level up your Laravel skills!

Each 2-minute email has real-world advice you can use.