Test-Driven APIs With Laravel and Pest Sample Chapter
Test-Driven APIs With Laravel and Pest Sample Chapter
This is a sample chapter from the original book that you can find here.
1 / 32
Martin Joo - Test-Driven APIs with Laravel and Pest Sample Chapter
Introduction
We live in the world of APIs. We write them, we use them, and often we hate them. As software developers,
we like patterns and standards. At this moment there are:
21 PSR standards
55 design patterns
24,128 ISO standards
Even with this many patterns, recommendations, and standards we often fail to write APIs that look similar
to each other and have great test coverage. We're arguing about the test pyramid and abstract concepts
instead of writing tests that really cover the whole API. We come up with unique solutions for general
problems because it feels satisfying.
For example, this is an API endpoint that returns products from a category:
GET /api/products?category_id=10
GET /api/products/10
POST /api/getProducts
Request body:
{
"category_id": 10
}
Behind the API we have controllers with 10+ methods and 500 lines of code. In my opinion, this API should
look like this:
GET /api/v1/categories/1dbd238c4-99ee-42d8-a7ae-b8602653de4c/products
In this book, I'm gonna show you how I build APIs that follow the JSON API standard and I'll show you how to
identify important use-cases that should be covered with tests. Other important topics I'll talk about:
2 / 32
Martin Joo - Test-Driven APIs with Laravel and Pest Sample Chapter
3 / 32
Martin Joo - Test-Driven APIs with Laravel and Pest Sample Chapter
The fundamentals
First of all, I would like to talk about the fundamentals. In this chapter, we won't write actual code but
discuss some basic concepts we will use in the rest of the book. If you're already familiar with:
Test-Driven Development
REST API
JSON API
feel free to skip this chapter and dive deep into the design. For the rest of us let's start with test-driven
development or TDD.
4 / 32
Martin Joo - Test-Driven APIs with Laravel and Pest Sample Chapter
Test-Driven Development
The red
Test-Driven development is a programming approach where the tests drive the actual implementation
code. In practice, this means that we write the tests first and then the production code. I know it sounds
complicated and maybe a little bit scary. So I try to rephrase it:
In Test-Driven Development the first step is to specify how you want to use your class / function.
Only after that do you start writing the actual code.
Let me illustrate it with a very simple example. I need to write a Calculator class that needs to be able to
divide two numbers. Let's use it before we write it!
5 / 32
Martin Joo - Test-Driven APIs with Laravel and Pest Sample Chapter
6 / 32
Martin Joo - Test-Driven APIs with Laravel and Pest Sample Chapter
Now, that we're using asserts we need a context. This is our test class:
It's always a good idea to separate your test functions based on 'use-cases'. In this basic example we have
two different scenarios:
7 / 32
Martin Joo - Test-Driven APIs with Laravel and Pest Sample Chapter
I don't know about you, but I'm going crazy just by looking at this function name:
testDivideWhenTheDividerIsZero . Fortunately, PHPUnit has a solution for us:
/** @test !%
public function it_should_return_zero_when_the_divider_is_zero()
{
!" !!&
}
}
8 / 32
Martin Joo - Test-Driven APIs with Laravel and Pest Sample Chapter
With the @test annotation you can omit the test word from the function name and by using underscores
you can actually read it. As you can see a test function's name reads like an English sentence: It should
divide valid numbers.
In this stage, you have no real code, only tests. So, naturally, these tests will fail (the Calculator class doesn't
even exist).
The green
It's time to write some code! After we wrote the tests, we can write the actual code:
class Calculator
{
public function divide(float $a, float $b): float
{
if ($b !!' 0.0) {
return 0;
} else {
return round($a / $b, 2);
}
}
}
In this stage, your main focus should not be to write the most beautiful code ever. No, you want to write the
minimum amount of code that makes the tests green. After your tests pass maybe you come up with a new
edge-case so you should write another test, and then handle this edge-case in your code.
The refactor
So we wrote the minimum amount of code to make the tests pass. This means that we have an automated
test for every use case we know right now. If you think about it, this is a huge benefit! From that point you
are almost unable to make bugs, so you can start refactoring:
9 / 32
Martin Joo - Test-Driven APIs with Laravel and Pest Sample Chapter
class Calculator
{
public function divide(float $a, float $b): float
{
try {
return round($a / $b, 2);
} catch (DivisionByZeroError) {
return 0;
}
}
}
This stage is called refactor because you don't write new code but make the existing codebase better.
Key takeaways:
When you write a test for a non-existing function you're writing the specification for that function.
10 / 32
Martin Joo - Test-Driven APIs with Laravel and Pest Sample Chapter
Red: specify how your interface should look like and what's the expected behavior
Green: write code that works
Refactor: it's time to apply some patterns, create factory methods, remove duplicated code
11 / 32
Martin Joo - Test-Driven APIs with Laravel and Pest Sample Chapter
REST API
The basics
REST or RESTful stands for representational state transfer which doesn't reveal much. In fact, it's just a way
to build APIs that expose resources. It's not a strict protocol (like SOAP) but a set of best practices and it can
be more fluid. It heavily relies on HTTP methods and status codes.
GET /api/v1/employees Get all employees 200 - OK, 404 - Not Found
POST /api/v1/employees Create a new employee 201 - Created, 429 - Unprocessable Entity
As you can see the whole API feels "resourceful". It's simple and exposes resources.
PUT vs PATCH
PUT and PATCH confuse a lot of developers, because both of them can be used in updates, but in fact, it's
simple:
A PATCH request is a partial update, so in the request, you only specify a few attributes.
A PUT request replaces the whole resource, so in the request you specify everything.
Let's say you have a list of posts. Each row has a button called 'Publish'. If you click on this button you can
specify when you want to publish the post. After that your app updates the publish_at attribute of the
Post model to the date, you specified. Something like this:
12 / 32
Martin Joo - Test-Driven APIs with Laravel and Pest Sample Chapter
PATCH /api/v1/posts/{post}/publish
{
"publish_at": "2022-03-10"
}
On the other hand, if you have an edit form for the post, when you click the save button the client will send
every attribute to the server doesn't matter if it's changed or not. This is a PUT request, because it replaces
the post on the server:
PUT /api/v1/posts/{post}
{
"title": "My post",
"body": "<div>Fancy content here!(div>",
"categoryId": 1,
}
A successful response after a PUT request should have no body. The client sent every attribute so
there's no reason to send back anything.
A successful response after a PATCH request should have a body. Maybe the API calculates something
from the attributes that the client sent.
A PUT request should be idempotent. This means I can write a bash script and send the PUT request
above 1000 times in a row after any side effect. The server remains in the same state.
A PATCH request is not necessarily idempotent.
13 / 32
Martin Joo - Test-Driven APIs with Laravel and Pest Sample Chapter
Nested resources
Another good thing that REST API taught us is that we can use nested resources in most situations. We will
build a mini CRM / Payroll management application later in this book. So we have employees and each
employee has many paychecks. Imagine the application has a dedicated page for an employee. On this
page, there's a list that shows all the paychecks for an employee.
At the API level, you have two choices. You can make the paycheck as a top-level resource:
GET /payhecks?employee_id=1
GET /employees/1/paychecks
It really depends on the exact situation but in most cases, you can and should go with the second option in
my opinion. How do you decide? You can answer these questions:
But even if the answer is yes, you can benefit from two separate API endpoints. It really depends
on the exact situation.
We will use this approach in the demo application later and I show you how the controllers follow the
nested resources.
Key takeaways:
14 / 32
Martin Joo - Test-Driven APIs with Laravel and Pest Sample Chapter
JSON API
Overview
We write so many APIs and if you think about it each of them is different:
The filtering and the sorting are different every damn time.
Wouldn't be great if there were a standard that generalizes all of these concepts? Yes, there is! And it's
called JSON API. Let's take a look:
15 / 32
Martin Joo - Test-Driven APIs with Laravel and Pest Sample Chapter
{
"data": [{
"type": "articles",
"id": 1,
"attributes": {
"title": "JSON:API paints my bikeshed!",
"body": "This is the body of the article",
"commentCount": 2,
"likeCount": 8
},
"relationships": {
"author": {
"links": {
"self": "http:!"example.com/articles/1/relationships/author",
"related": "http:!"example.com/articles/1/author"
}
},
"comments": [{
"data": { "type": "user", "id": 9}
}]
},
"links": {
"self": "http:!"example.com/articles/1"
}
}],
"included": [{
"type": "comments",
"id": "5",
"attributes": {
"body": "First!"
},
"links": {
"self": "http:!"example.com/comments/5"
}
}]
}
16 / 32
Martin Joo - Test-Driven APIs with Laravel and Pest Sample Chapter
I know, I know... At first, it looks like hell, so let's interpret it step by step.
Identification
{
"id": 1,
"type": "articles"
}
Every response has these two keys no matter what. The id is straightforward, the type is the plural form of
the resource.
Attributes
{
"attributes": {
"title": "JSON:API paints my bikeshed!",
"body": "This is the body of the article",
"commentCount": 2,
"likeCount": 8
}
}
The attributes key contains the actual data. I bet that in most of your APIs this is the top-level data.
Relationships
This is the more interesting stuff. In web development we have two main choices when it comes to
relationships in APIs:
There are pros and cons to both of these approaches and most of the time it depends on your application.
But there is two good rules of thumbs:
If you include (almost) everything you may end up with huge responses and N+1 query problems. For
example, you return a response with 50 articles and each article loads the author. You end up with 51
queries.
If you include (almost) nothing you have small responses but you may have too many requests to the
server. For example, you return a response with 50 articles, after that the client makes 50 additional
HTTP requests to get the authors.
17 / 32
Martin Joo - Test-Driven APIs with Laravel and Pest Sample Chapter
{
"relationships": {
"author": {
"links": {
"self": "http:!"example.com/articles/1/relationships/author",
"related": "http:!"example.com/articles/1/author"
}
},
"comments": [{
"data": {"type": "comments", "id": 5}
}]
},
}
In this example, the comments relationship is loaded while the author only contains a link where you can
fetch the author. In fact, it contains two links and it's a little bit confusing so let's make it clear:
related: with this URL I can manipulate the actual user (author). So if I make a DELETE
http://example.com/articles/1/author request I delete the user from the users table.
self: this URL only manipulates the relationship between the author and the article. So if I make a
DELETE http://example.com/articles/1/relationships/author request I only destroy the
relationship between the user and the article. But it won't delete the user from the users table.
As you can see in the comments key there is an ID and a type, but no attributes. Where is it? It lives in the
included key:
"included": [{
"type": "comments",
"id": "5",
"attributes": {
"body": "First!"
},
"links": {
"self": "http:!"example.com/comments/5"
}
}]
18 / 32
Martin Joo - Test-Driven APIs with Laravel and Pest Sample Chapter
Relationships: it only contains information about the relationships. It tells you that an article has author
and comments relationships.
Included: it contains the loaded relationships. In the example above the API endpoint only loads the
comments, so in the included key you can find the comments but not the author. The author can be
fetched using the links in the relationships object.
This is the part I like the least about JSON APi. In my opinion, it would be much better if there's only a
relationships key that looks like this:
{
"relationships": {
"author": {
"links": {
"self": "http:!"example.com/articles/1/relationships/author",
"related": "http:!"example.com/articles/1/author"
}
},
"comments": [{
"type": "comments",
"id": "5",
"attributes": {
"body": "First!"
},
"links": {
"self": "http:!"example.com/comments/5"
}
}]
},
}
So it actually contains the comments and provides links for the author. However in this book, I will follow
the official specification, but here's my opinion: you don't have to obey a standard just because it's a
standard. You can use only the good parts from it that fit your application.
Either way, you have a reasonable default standard in your company / projects.
19 / 32
Martin Joo - Test-Driven APIs with Laravel and Pest Sample Chapter
Requests
# Filtering by title
GET /articles?filter[title]=laravel
# Including relationships
GET /articles?include=author,comments
As you can see the client is in full control of what and how it wants to query. Fortunately, the Laravel
community has two amazing packages that make the requests and also the responses very easy to use:
@timacdonald87 created a package called json-api for Laravel. This package extends the base
JsonResource class and implements (at the time) most of the specifications.
@spatie_be created a package called laravel-query-builder. I will talk about it in more detail, but it
implements all of the filterings, sorting logic with Eloquent.
Key takeaways:
The JSON API provides a very reasonable standard for use to generalize API requests and responses.
Querying relationships or providing links depends on your application, or in fact the exact scenario but
JSON API supports both.
You should check out Tim Macdonald's Youtube videos where he builds the json-api package.
20 / 32
Martin Joo - Test-Driven APIs with Laravel and Pest Sample Chapter
Use the HTTP status codes. For example, if your endpoint is asynchronous (meaning it dispatches jobs
that will take time) don't return 200 but 202 - Accepted and a link where the client can request the
status of the operation.
Don't expose auto-increment integer IDs! This is an important one. Using IDs like 1, 2 in your URLs is a
security concern and can be exploited. It's especially important if your API is public. This is how I do it:
I have integer IDs in the database. It's mostly MySQL which is optimized for using auto-increment
IDs.
I also have UUIDs for almost every model.
In the API I only expose the UUIDs.
Versioning your API. This is also more important if you write a public API. I show you later how to do it
easily with Laravel's RouteServiceProvider.
Loading relationships
FIltering
Sorting
Sparse fieldsets
Try to standardize these things so you have the same solutions in your projects. In this case, we're
gonna use JSON API.
Make your response easy to use for the client, for example:
All of the above is easy to achieve in Laravel with the help of a few packages.
GET /api/v1/employees/a3b5ce95-c9c8-40f7-b8d7-06133c768a92?
include=department,paychecks
GET /api/v1/emplyoees/a3b5ce95-c9c8-40f7-b8d7-06133c768a92/paychecks?sort=-
payed_at
21 / 32
Martin Joo - Test-Driven APIs with Laravel and Pest Sample Chapter
{
"data": {
"id": "a3b5ce95-c9c8-40f7-b8d7-06133c768a92",
"type": "employees",
"attributes": {
"fullName": "Adalberto Yost V",
"email": "[email protected]",
"jobTitle": "Full stack developer",
"payment": {
"type": "salary",
"amount": {
"cents": 5184700,
"dollars": "$51,847.00"
}
}
},
"relationships": {
"department": {
"data": {"id": "e12cea58-d2ec-495a-a542-03fd29872d4c", "type":
"departments"}
}
},
"meta": {},
"links": {
"self": "http:!"localhost:8000/api/v1/employees/a3b5ce95-c9c8-40f7-
b8d7-06133c768a92"
}
},
"included": [
{
"id": "e12cea58-d2ec-495a-a542-03fd29872d4c",
"type": "departments",
"attributes": {
"name": "Development",
},
22 / 32
Martin Joo - Test-Driven APIs with Laravel and Pest Sample Chapter
"relationships": {},
"meta": {},
"links": {
"self": "http:!"localhost:8000/api/v1/departments/e12cea58-d2ec-495a-
a542-03fd29872d4c"
}
}
]
}
23 / 32
Martin Joo - Test-Driven APIs with Laravel and Pest Sample Chapter
The design
In this chapter I'd like to talk about the design of the demo application we're gonna build. It's a mini CRM /
payroll system where we users can manage:
Departments
Employees
WIth fix salary
Or hourly rate
Time logs
It will create paychecks for each user based on their salary or hourly rate.
24 / 32
Martin Joo - Test-Driven APIs with Laravel and Pest Sample Chapter
User stories
Name Description
Create a Users can create a new department with a simple name and description (details
department later).
Update a
Users can update an existing department.
department
List all Users can list all departments. We need to show the number of employees for
departments each departments.
Show a Users can see the details of a department. We need to list all the employees in the
department department.
Create an
Users can create new employees (details later).
employee
Update an
Users can update an existing employee.
employee
List all
Users can list all employees. They can be filtered (details later).
employees
Show an Users can see the details of an employee. We need to list all the paychecks for an
employee employee.
Delete an Users can delete an employee. We don't want to loose important data like
employee paychecks or time logs.
Users can pay all the employees. We need to create paychecks for employees with
Payday
the appropriate amount.
25 / 32
Martin Joo - Test-Driven APIs with Laravel and Pest Sample Chapter
Database design
department:
description: It holds information about a department inside a company like
Development
attributes:
- name
- description
relationships:
- employees: A department has many employees
- paychecks: A department has many paychecks through employees
employee:
description: It holds information about an employee and his / her salary
attributes:
- full_name
- email
- department_id
- job_title
- payment_type:
- salary
- hourly_rate
- salary: Only if payment_type is salary
- hourly_rate: Only if payment_type is hourly_rate
relationships:
- department: An employee belongs to a department
- paychecks: An employee has many paychecks
- time_logs: An employee has many time_logs
paycheck:
description: It holds information about an employee paycheck
attributes:
- employee_id
- net_amount
- payed_at
relationships:
26 / 32
Martin Joo - Test-Driven APIs with Laravel and Pest Sample Chapter
time_logs:
description: Time tracked by an employee
attributes:
- employee_id
- started_at
- stopped_at
- minutes
relationships:
- employee: A time_log belongs to an employee
We need to store amounts in the employee and in the paycheck tables. We can store them as float
numbers, but in this case maybe we face some rounding issues. You know neither PHP nor Javascript
(our client) is with types and numbers. Or we can store them as cents value in integer but expose them
as dollar value in float. In this case rounding is not an issue, but we have to convert the values
somewhere.
As you can see in the time_logs table I used minutes instead of hours. This is the same as cents vs
dollars. I feel like if we store them as minutes like 90 instead of 1.5 we have less issues and more
flexibility later.
There are two payment types: salary and hourly rate. I feel like it's a good opportunity to use enums,
factories, and the strategy design pattern!
As you can see I follow the Laravel "standard" when it comes to dates. So instead of date I call the
column payed_at or started_at . Not a big deal but these names feel more natural.
API design
From the "user stories" we can easily plan our API:
POST /departments
PUT /departments
GET /departments
GET /departments/{department}
GET /departments/{department}/employees
POST /employees
PUT /employees
GET /employees
GET /employees/{employee}
27 / 32
Martin Joo - Test-Driven APIs with Laravel and Pest Sample Chapter
DELETE /employees/{employee}
GET /employees/{employee}/payhecks
POST /payday
That's the bare minimum our application needs. Maybe there's gonna be more than that, but that's okay for
now. I use PUT for update endpoints by the way. Later I will discuss these endpoints in more detail, but I
guess you already have a broad idea about the application.
28 / 32
Martin Joo - Test-Driven APIs with Laravel and Pest Sample Chapter
Pest
In this book, I'm gonna use Pest which is an amazing testing framework for PHP.
A traditional API test in Laravel looks like this (it's not a complete test, just a short example):
/** @test !%
public function it_should_return_an_employee()
{
$employee = $this!#getJson(route('emplyoees'))!#json('data');
PHPUnit has assert functions like: assertEquals, assertEmpty and we can use them to test our values.
Pest takes a more functional approach and the same test looks like this:
expect($employee)
!#name!#toBe('John Doe')
!#payment!#type!#toBe('salary')
!#payment!#amount!#dollars!#toBe('$50,000');
});
As the first step, we wrap our value (it can be a scalar, an array, or an object) using the expect
function.
After that, we can access the properties (like name or type) from the original array as an object.
And we can use expectations like the toBe which checks that two values are the same.
29 / 32
Martin Joo - Test-Driven APIs with Laravel and Pest Sample Chapter
expect($employee)
!#name!#toBe('John Doe');
The good thing about Pest is that you chain multiple things after each other:
So you can mix the attributes with the Pest expectations. I know it feels like magic, but it's really easy to use.
By the way, the implementation is very similar to Laravel's higher-order collections. Pest has a class called
HigherOrderExpectation. This class does the magic part. You don't have to worry about it but feel free to
check the source code.
By the way, we need to wrap every test in a function called it . This will tell Pest that it's a test and it runs it
when we say:
./vendor/bin/pest
Pest can do so much more than this. For example, later we'll use sequences to expect collections.
30 / 32
Martin Joo - Test-Driven APIs with Laravel and Pest Sample Chapter
Value Objects: they help us work with scalar data by wrapping them in an object. For example, we can
create a Money class that holds an integer value and helps us convert from cents to dollars and also
can format as a string.
DataTransferObjects (DTO): they will help us work with associative arrays. Instead of a big array where
you have undocumented, unknown array keys, you have a dead-simple object where each array key is
a type-hinted well-documented property.
Actions: we use them to express our user stories as stand-alone, reusable classes. They're not really
from the DDD, I think they were invented by the Laravel community but they are the successor of the
Service classes.
Key takeaways:
31 / 32
Martin Joo - Test-Driven APIs with Laravel and Pest Sample Chapter
About me
I’m Martin Joo, a software engineer since 2012. I’m passionate about Domain-Driven Design, Test-Driven
Development, Laravel, and beautiful code in general.
You can find me on Twitter @mmartin_joo where I post tips and tricks about PHP, Laravel every day. If you
have questions just send me a DM on Twitter.
This is a sample chapter from the original book that you can find here.
32 / 32