0% found this document useful (0 votes)
10 views93 pages

Martin Joo - Test-Driven APIs With Laravel and Pest

The document discusses building test-driven APIs using Laravel and Pest, emphasizing the importance of Test-Driven Development (TDD) and RESTful principles. It covers key concepts such as the red-green-refactor cycle, API design best practices, and the JSON API standard. The author aims to provide a structured approach to creating consistent and well-tested APIs.
Copyright
© © All Rights Reserved
We take content rights seriously. If you suspect this is your content, claim it here.
Available Formats
Download as PDF, TXT or read online on Scribd
0% found this document useful (0 votes)
10 views93 pages

Martin Joo - Test-Driven APIs With Laravel and Pest

The document discusses building test-driven APIs using Laravel and Pest, emphasizing the importance of Test-Driven Development (TDD) and RESTful principles. It covers key concepts such as the red-green-refactor cycle, API design best practices, and the JSON API standard. The author aims to provide a structured approach to creating consistent and well-tested APIs.
Copyright
© © All Rights Reserved
We take content rights seriously. If you suspect this is your content, claim it here.
Available Formats
Download as PDF, TXT or read online on Scribd
You are on page 1/ 93

TEST-DRIVEN APIS WITH

LARAVEL AND PEST

<?php

it('has a beautiful API')


->get('/api/v1')
->assertOk();

By Martin Joo

AN EASY WAY TO GET STARTED


WITH TEST-DRIVEN
DEVELOPMENT
Martin Joo - Test-Driven APIs with Laravel and Pest

Introduction
The fundamentals
Test-Driven Development
The red
The green
The refactor
REST API
The basics
PUT vs PATCH
Nested resources
JSON API
Overview
Identification
Attributes
Relationships
Requests
API Best Practices
The design
User stories
Database design
API design
Pest
Useful concepts from Domain-Driven Design
The implementation
Blueprint
API versioning
UUIDs
Configuring Pest
Department API
Create a department
Update a department
Get departments
Resources
Employee API
Payment types
Create / Update an employee
Get employees
Get department employees
Resources and value objects
Payday API
Create paychecks
Get paychecks for an employee
Thank you

1 / 92
Martin Joo - Test-Driven APIs with Laravel and Pest

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 in a category:

GET /api/products?category_id=10

The same API in a different application:

GET /api/products/10

The same API from your favorite legacy project:

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

And it should map to CategoryProductController::index , and also a GetCategoryProductsTest.php


should exist.

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.

2 / 92
Martin Joo - Test-Driven APIs with Laravel and Pest

Other important topics I'll talk about:

How to test APIs with Pest.


How to use JSON API to simplify your decisions about API design.
How you can use PHP 8.1 enums with factories and the Strategy design pattern.
And a lot more about API design.

3 / 92
Martin Joo - Test-Driven APIs with Laravel and Pest

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 / 92
Martin Joo - Test-Driven APIs with Laravel and Pest

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!

$calculator = new Calculator();

!" I expept to be 5.00


$result = $calculator!#divide(10, 2);

!" I expect to be 3.33


$result = $calculator!#divide(10, 3);

!" I expect to be 0.00 instead of an Exception


$result = $calculator!#divide(10, 0);

After 3 examples we have our specification:

Always returns float with 2 decimals


Returns 0 when division by zero happens

Instead of comments, we can use so-called assertions from PHPUnit:

5 / 92
Martin Joo - Test-Driven APIs with Laravel and Pest

$calculator = new Calculator();

$result = $calculator!#divide(10, 2);


$this!#assertEquals(5.00, $result);

$result = $calculator!#divide(10, 3);


$this!#assertEquals(3.33, $result);

$result = $calculator!#divide(10, 0);


$this!#assertEquals(0.00, $result);

6 / 92
Martin Joo - Test-Driven APIs with Laravel and Pest

Now, that we're using asserts we need a context. This is our test class:

class CalculatorTest extends TestCase


{
public function testDivide()
{
$calculator = new Calculator();

$this!#assertEquals(5.00, $calculator!#divide(10, 2));


$this!#assertEquals(3.33, $calculator!#divide(10, 3));
$this!#assertEquals(0.00, $calculator!#divide(10, 0));
}
}

It's always a good idea to separate your test functions based on 'use-cases'. In this basic example we have
two different scenarios:

Divide two valid numbers


Handle division by zero

7 / 92
Martin Joo - Test-Driven APIs with Laravel and Pest

So we should create two test functions:

class CalculatorTest extends TestCase


{
public function testDivideValidNumbers()
{
$calculator = new Calculator();

$this!#assertEquals(5.00, $calculator!#divide(10, 2));


$this!#assertEquals(3.33, $calculator!#divide(10, 3));
}

public function testDivideWhenTheDividerIsZero()


{
$calculator = new Calculator();

$this!#assertEquals(0.00, $calculator!#divide(10, 0));


}
}

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:

class CalculatorTest extends TestCase


{
/** @test !$
public function it_should_divide_valid_numbers()
{
}

/** @test !$
public function it_should_return_zero_when_the_divider_is_zero()
{
}
}

8 / 92
Martin Joo - Test-Driven APIs with Laravel and Pest

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.

A test should read like documentation for a class / method.

In this stage, you have no real code, only tests. So, naturally, these tests will fail (the Calculator class doesn't
even exist).

This stage is called red because your tests fail.

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.

This stage is called green because your tests pass.

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 / 92
Martin Joo - Test-Driven APIs with Laravel and Pest

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.

The whole TDD cycle looks like this:

10 / 92
Martin Joo - Test-Driven APIs with Laravel and Pest

Key takeaways:

When you write a test for a non-existing function you're writing the specification for that function.
Your tests should read like documentation / specification.

In the red-green-refator cycle you're wearing only one hat at a time:


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 / 92
Martin Joo - Test-Driven APIs with Laravel and Pest

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.

Here are some examples:

Method URI Description Typical status codes

GET /api/v1/employees Get all employees 200 - OK

GET /api/v1/employees/{employee} Get one employee 200 - OK, 404 - Not Found

POST /api/v1/employees Create a new employee 201 - Created, 429 - Unprocessable Entity

Update an existing 204 - No Content, 404 - Not Found, 429 - Unprocessable


PUT /api/v1/employees
employee Entity

DELETE /api/v1/employees/{employee} Delete an employee 204 - No Content, 404 - Not Found

Any endpoint can return the following status codes:

401 - Unauthorized: User not logged in


403 - Forbidden: User has no permission to perform the operation
409 - Conflict: Edit conflict between multiple simultaneous updates
429 - Too Many Requests: Also known as API throttling
418 - I'm a teapot

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 / 92
Martin Joo - Test-Driven APIs with Laravel and Pest

This is a PATCH request because it's partial. It looks like this:

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,
}

Some other differences:

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 / 92
Martin Joo - Test-Driven APIs with Laravel and Pest

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.

On the API level, you have two choices. You can make the paycheck a top-level resource:

GET /payhecks?employee_id=1

Or you can make it a nested resource:

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:

Can a paycheck live without an employee? Not really.


Do I need a page where I list all of the paychecks in one huge list? Probably not.

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:

REST is a style for building APIs. It defines some best practices.


It's resourceful, and it achieves this by relying on HTTP methods and status codes.
PATCH is a partial update (publish button) while PUT replaces a whole resource (edit form).
You should check out @adamwathan's video called Cruddy by design where he talks about (mostly)
nested resources.

14 / 92
Martin Joo - Test-Driven APIs with Laravel and Pest

JSON API
Overview
We write so many APIs and if you think about it each of them is different:

Different data structures.

Relationships are included differently.

In some APIs, a relationship is included with other attributes.


In some APIs, there's a separate relations key for them.
In some APIs you don't get relationships at all, you have to call another endpoint.
Some APIs use camelCase while others use snake_case .

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 / 92
Martin Joo - Test-Driven APIs with Laravel and Pest

{
"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 / 92
Martin Joo - Test-Driven APIs with Laravel and Pest

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:

Include (almost) everything and return a huge response.


Include (almost) nothing and provide links to the clients.

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 / 92
Martin Joo - Test-Driven APIs with Laravel and Pest

JSON API supports both of these options:

{
"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 / 92
Martin Joo - Test-Driven APIs with Laravel and Pest

This is a little bit confusing, so let's make it clear:

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 / 92
Martin Joo - Test-Driven APIs with Laravel and Pest

Requests

# Sorting by likes asc


GET /articles?sort=likes

# Sorting by likes desc


GET /articles?sort=-likes

# Filtering by title
GET /articles?filter[title]=laravel

# Filtering by the authour's name


GET /articles?filter[author.name]=taylor

# Including relationships
GET /articles?include=author,comments

# Get only the title and body fields


GET /articles?fields[articles]=title,body

# Include the author and select specific fields


GET /articles?include=author&fields[articles]=title,body&fields[author]=name

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 / 92
Martin Joo - Test-Driven APIs with Laravel and Pest

API Best Practices


After all these fundamentals, let me share some of my API "best practices" with you:

Write easy-to-understand, resourceful APIs.

Use nested resources with nested controllers whenever it makes sense.

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.

Let the client decide what it wants. This means:

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:

Use camelCase which is very natural in Javascript.


Use nested attributes for cohesive data.

All of the above is easy to achieve in Laravel with the help of a few packages.

To sum it up, we want API endpoints like these:

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 / 92
Martin Joo - Test-Driven APIs with Laravel and Pest

And responses like this one:

{
"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 / 92
Martin Joo - Test-Driven APIs with Laravel and Pest

"relationships": {},
"meta": {},
"links": {
"self": "http:!"localhost:8000/api/v1/departments/e12cea58-d2ec-495a-
a542-03fd29872d4c"
}
}
]
}

23 / 92
Martin Joo - Test-Driven APIs with Laravel and Pest

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 the users can manage:

Departments

Employees
With fix salary
Or hourly rate
Time logs

Hourly rate employees have time logs.


Pay employees

It will create paychecks for each user based on their salary or hourly rate.

Without further ado let's see the user stories.

24 / 92
Martin Joo - Test-Driven APIs with Laravel and Pest

User stories
Name Description

Create a
Users can create a new department with a simple name and description.
department

Update a
Users can update an existing department.
department

List all
Users can list all departments.
departments

Show a Users can see the details of a department. We need to list all the employees in
department the department.

Create an
Users can create new employees.
employee

Update an
Users can update an existing employee.
employee

List all
Users can list all employees. They can be filtered.
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
Payday
with the right amount.

The main business logic is:

An employee has a payment type. It's salary or hourly rate.


If an employee paid by the hour he or she has time logs.
When it's payday we need to calculate the right amount for each employee and create a paycheck.

25 / 92
Martin Joo - Test-Driven APIs with Laravel and Pest

Database design

department:
description: It holds information about a department inside a company
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:
- employee: A paycheck belongs to an employee

26 / 92
Martin Joo - Test-Driven APIs with Laravel and Pest

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

It's not complicated at all, but we have some interesting questions:

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 great with types and numbers. Or we can store them as cents in integer but expose them
as dollar value. 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.

27 / 92
Martin Joo - Test-Driven APIs with Laravel and Pest

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}
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 / 92
Martin Joo - Test-Driven APIs with Laravel and Pest

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');

$this!#assertEquals('John Doe', $employee['name']);


$this!#assertEquals('salary', $employee['payment']['type']);
$this!#assertEquals('$50,000', $employee['payment']['amount']['dollars']);
}

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:

it('should return an employee', function () {


$employee = getJson(route('emplyoees'))!#json('data');

expect($employee)
!#name!#toBe('John Doe')
!#payment!#type!#toBe('salary')
!#payment!#amount!#dollars!#toBe('$50,000');
});

A few important notes:

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 / 92
Martin Joo - Test-Driven APIs with Laravel and Pest

expect($employee)
!#name!#toBe('John Doe');

!" Is the same as:


$employee['name'] !!% 'John Doe'

The good thing about Pest is that you chain multiple things after each other:

!" name is a key from the original array


!#name
!" toBe is a function provided by Pest
!#toBe('John Doe')
!" payment and type are keys from the original array
!#payment!#type
!" toBe is a function provided by Pest
!#toBe('salary')

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 / 92
Martin Joo - Test-Driven APIs with Laravel and Pest

Useful concepts from Domain-Driven Design


In this book I will use some concepts from the DDD world, namely:

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
becomes 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:

Pest is an awesome, functional testing framework.


You don't have to go all-in to use a programming approach like DDD. You can use just a handful of
concepts that fits your application.
You should check out @JustSteveKing's Youtube channel where he constantly uses these DDD
techniques to write better code.
If you're not sure what those DDD things are you can read my blog posts, but later I try to explain them
with examples.

31 / 92
Martin Joo - Test-Driven APIs with Laravel and Pest

The implementation
The repository for this project can be found here.

In this section we're gonna implement the payroll application step-by-step. But first, I want to introduce you
a very useful Laravel package called Blueprint.

Blueprint
You can define your tables in a yaml file, and it will create:

Migrations
Models
Factories
And even controllers with CRUD actions

from it. The configuration looks like this:

models:
Department:
uuid: uuid
name: string:50
description: longtext
relationships:
hasMany: Employee
Employee:
uuid: uuid
full_name: string:100
email: string:100 index
department_id: id foreign
job_title: string:50
payment_type: string:20
salary: integer unsigned nullable
hourly_rate: integer unsigned nullable
relationships:
hasMany: Paycheck, Timelog

I don't want to list the whole file here you can see it in the repository, it's called draft.yaml .

32 / 92
Martin Joo - Test-Driven APIs with Laravel and Pest

API versioning
API versioning means two things:

Append the version number to your URL.


Have a dedicated route file for each version.

Let's look at these endpoints:

/api/v1/employees
/api/v2/employees

In this case there are two route files in the project:

/routes/api/v1.php
/routes/api/v2.php

In order make this possible we need to modify the boot method in the RouteServiceProvider :

public function boot()


{
$this!#configureRateLimiting();

$this!#routes(function () {
Route!'prefix('api/v1')
!#middleware(['api', 'auth:sanctum'])
!#group(base_path('routes/api/v1.php'));

Route!'prefix('api/v2')
!#middleware(['api', 'auth:sanctum'])
!#group(base_path('routes/api/v2.php'));
});
}

As you can see we can also apply middlewares to these routes so we don't need to write them in the route
files. That's it! Now we have API endpoints with versions.

One more thing: in an application where you only have API routes (like this demo app) you don't really need
the api prefix, but for some reason I like it...

33 / 92
Martin Joo - Test-Driven APIs with Laravel and Pest

UUIDs
As I mentioned earlier in my opinion is best practice to only expose UUIDs in your URLs. In order to do this
we need a UUID column in every table. There are a few things to do.

First, add uuid in the migrations:

Schema!'create('employees', function (Blueprint $table) {


$table!#id();
$table!#uuid('uuid');

!" !!(
});

Once again:

id will be used in the queries. This is the business logic layer.


uuid will be used in the routes and resources (with route model binding). This is the API layer.

Next, we need a trait that can be used with any model:

use Ramsey\Uuid\Uuid;

trait HasUuid
{
public static function bootHasUuid(): void
{
static!'creating(function (Model $model): void {
$model!#uuid = Uuid!'uuid4()!#toString();
});
}
}

It registers a model event called creating. This will run every time when we run an insert query (so
creating a new model). It's important to note that this method will not run on update. And this is good. We
don't want to generate a new UUID every time we update a model.

The last thing is to use this trait and tell Eloquent that we want to bind our models with the uuid column:

34 / 92
Martin Joo - Test-Driven APIs with Laravel and Pest

class Employee extends Model


{
use HasFactory;
use HasUuid;
use SoftDeletes;

public function getRouteKeyName(): string


{
return 'uuid';
}
}

The getRouteKeyName method is used by Laravel when resolving a model instance from a route. For
example:

Route!'get('employees/{employee}', [EmployeeController!'class, 'show']);

When Laravel sees the {employee} parameter in the route it executes a query similar to this:

select * from employees where id = 1

But if you override the getRouteKeyName method and return uuid the query will look like this:

select * from employees where uuid = "e12cea58-d2ec-495a-a542-03fd29872d4c"

And that's exactly what we want. Of course if you want you can create an abstract class called Model or
BaseModel and override the getRouteKeyName method there.

By the way, I like to put every Model related trait, interface, or abstract class under the
App\Models\Concerns namespace.

35 / 92
Martin Joo - Test-Driven APIs with Laravel and Pest

Configuring Pest
One last thing before we start writing code. There is a Pest.php file in the tests folder. In this file there's
a very important line:

uses(Tests\TestCase!'class, LazilyRefreshDatabase!'class)!#in('Feature');

TestCase is a class provided by Laravel (also lives in the tests folder). This class has a lot of helpers that
we can use with Laravel, most importantly:

Log in using Sanctum.


Sending HTTP requests and parsing JSON responses.

LazilyRefreshDatabase also provided by Laravel and it will refresh the database before every test. It will:

Run the migrations if needed.


Wrap every test function in a transaction (so it won't insert anything in the database).
But it only does these things if the test interacts with the database.

If you want to learn more about this trait you should watch this short video from Laracasts.

When we write tests with Pest we don't actually write test classes. It's just a file with functions in it so this is
why we need to do these things in a separate Pest.php config file.

36 / 92
Martin Joo - Test-Driven APIs with Laravel and Pest

Department API
Now let's start with the first API. That's gonna be the department API since this is the most simple and it
doesn't have any dependencies (like the Payday API requires both employees and departments).

Create a department
This endpoint will accept a name and a description attribute from the request. Only the name is
required.

Without further ado, let's write the first test in this amazing application:

use function Pest\Laravel\postJson;

it('should create a department', function () {


$department = postJson(route('departments.store'), [
'name' !) 'Development',
'description' !) 'Awesome developers across the board!',
])!#json('data');

expect($department)
!#attributes!#name!#toBe('Development')
!#attributes!#description!#toBe('Awesome developers across the board!');
});

As I said before it's not a class, it's a file with a bunch of it functions in it. The postJson is a helper
method provided by Laravel. It comes from the TestCase class and the pest-plugin-laravel makes it
possible to use it as a simple function. This is why we need to configure Pest to use the TestCase class in
the Pest.php file.

So the postJson will send a POST request and it will send the data as JSON. On the other hand, the -
>json('data') call is parsing the response. It will parse as JSON and it takes the data key from the
response. Don't forget that JSON API wraps everything into a data key.

After we send the request and parse the response we need to check the result. In this case, we simply check
the name and description attributes. Don't forget that JSON API wraps every attribute in a key called
attributes .

And as I showed you earlier you can chain attributes and Pest functions after each other.

So this is our first test:

We post a name and a description to an endpoint.


And we expect to get back the new department in the response.

37 / 92
Martin Joo - Test-Driven APIs with Laravel and Pest

Now let's see the unhappy path. The name of the department is required and it needs to be unique. We can
cover this requirement with a test like this:

it('should return 422 if name is invalid', function (?string $name) {


Department!'factory([
'name' !) 'Development',
])!#create();

postJson(route('departments.store'), [
'name' !) $name,
'description' !) 'description',
])!#assertInvalid(['name']);
})!#with([
'',
null,
'Development'
]);

First, let's clarify the Pest specific parts. I chained the with function after the it , and I also gave a ?string
$name parameter to the callback function. The with method simply gets an element out of the array and
calls the callback with it. So under the hood, it does something like this:

foreach (['', null, 'Development'] as $name) {


$theActualTest($name);
}

$theActualTest = function (?string $name) {


Department!'factory([
'name' !) 'Development',
])!#create();

postJson(route('departments.store'), [
'name' !) $name,
'description' !) 'description',
])!#assertInvalid(['name']);
};

I hope it makes it more clear. It's a very good way to run the same test with different values, in this case:

38 / 92
Martin Joo - Test-Driven APIs with Laravel and Pest

An empty string ''


null
And the string Development

We expect the following:

The empty string and the null will fail because the name is required.
The string Development will fail because at the beginning of the test we create a department with this
very name. So it's already taken.

After the postJson we call the assertInvalid helper. This method comes from the TestResponse class
and is provided by Laravel. It's a great way to assert HTTP-specific things on the Response object.
assertInvalid is a recent addition to Laravel 8.x. If you have older Laravel you can use this:

$response!#assertStatus(Response!'HTTP_UNPROCESSABLE_ENTITY);

That's it! We covered the whole POST /departments API with tests. Before we move on I want to recap the
anatomy of a test:

Given
When
Then

The given describes our environment, the setup. What needs to be initialized or created to test this feature?
The when describes the action we want to trigger. What feature I'd like to test? And finally, the then shows
us what's the expected behavior. In our case, it looks like:

it('should return 422 if name is invalid', function (?string $name) {


!" Given
Department!'factory([
'name' !) 'Development',
])!#create();

!" When
postJson(route('departments.store'), [
'name' !) $name,
'description' !) 'description',
])

!" Then
!#assertInvalid(['name']);
})!#with([

39 / 92
Martin Joo - Test-Driven APIs with Laravel and Pest

'',
null,
'Development'
]);

It reads like an English sentence:

Given I have a department called 'Development'


When I call the POST API with the name 'Development'
Then I except the response to be invalid

As you can see in the repository, I create a separate test file for each user story, so in this case:

CreateDepartmentTest
UpdateDepartmentTest

If you run these tests with ./vendor/bin/pest they will fail of course, so it's time to write some actual
code. Earlier I talked about DTOs, let's start with them:

namespace App\DataTransferObjects;

class DepartmentData
{
public function !*construct(
public readonly string $name,
public readonly ?string $description
) {}
}

It's a really simple class that holds data. All the data that necessary to create a new department. As we move
forward I try to explain why they are good.

Next here's the action class:

40 / 92
Martin Joo - Test-Driven APIs with Laravel and Pest

namespace App\Actions;

use App\DataTransferObjects\DepartmentData;
use App\Models\Department;

class CreateDepartmentAction
{
public function execute(DepartmentData $departmentData): Department
{
return Department!'create([
'name' !) $departmentData!#name,
'description' !) $departmentData!#description,
]);
}
}

You can see that an action class is almost like a user story. A user can create a department, so we have a
class called CreateDepartmentAction . An action class always has an execute method. In this case, it gets
a DepartmetnData . This is the first advantage of using DTOs. Instead of an associative array, we have an
object that holds type-hinted properties. In the case of an array, you can only tell what keys it has if you dig
into the source code. I know that this is just a function with 3 lines of code, but imagine a 10 years old legacy
application where a function contains 300 (or 3000) lines of code. What would you have in that situation?
Random associative arrays or DTOs? I would go with DTOs.

41 / 92
Martin Joo - Test-Driven APIs with Laravel and Pest

And finally the Controller:

class DepartmentController extends Controller


{
public function !*construct(
private readonly CreateDepartmentAction $createDepartment
) {}

public function store(StoreDepartmentRequest $request)


{
$departmentData = new DepartmentData(!!($request!#validated());
$department = $this!#createDepartment!#execute($departmentData);

return DepartmentResource!'make($department)
!#response()
!#status(Response!'HTTP_CREATED);
}
}

42 / 92
Martin Joo - Test-Driven APIs with Laravel and Pest

This line leverages PHP8 named arguments:

$departmentData = new DepartmentData(!!($request!#validated());

$request->validated() returns an associative array like this:

[
"name": "Development",
"description": "Awesome developers accross the board!",
]

The ... operator will spread the elements of this array and named arguments makes it possible to accept
them as separate arguments. So the $array['name'] will be passed as the name argument for the
constructor.

A quick summary:

The Controller makes a DTO from the Request


It passes the DTO to the Action class
The Action will run the actual query and returns the new department
The Controller returns a Resource (we'll talk about resources soon)

You might think: "This is way more complicated than it should be. I'm out of here." Why not just do
something like this:

class DepartmentController extends Controller


{
public function store(StoreDepartmentRequest $request)
{
$department = Department!'create($request!#validated());

return DepartmentResource!'make($department)
!#response()
!#status(Response!'HTTP_CREATED);
}
}

In this particular example, it's easier to do it in the controller, yes. But don't forget that the department is a
dead-simple model and this is a demo application. In the real world models and user stories are way more
complicated than this. I mean way more complicated. Just to name a few extra steps:

43 / 92
Martin Joo - Test-Driven APIs with Laravel and Pest

Sending e-mails and notifications

Dispatching jobs
Logging

Communicating with 3rd party services


Handling permissions

Processing complicated data structures. For example, the request contains:

A course
With 15 lessons
Each lesson has some content like questions, tests, images, videos, links
Each lesson has some deadline like Lesson #5 needs to be finished 10 days after a user started
the course
The course has an audience
It has a schedule like Lesson #1 will be published on 2022-03-01, Lesson #2 will be published 5
days after that

So we obviously don't want to implement everything in the Controller. The natural next step is to move
some logic in the models. After two weeks you will end up with a User model that contains 5000 lines of
code and has 30 use statements at the beginning of the class. I can guarantee it! Every project ends up
with a monster User model.

And when you work on a larger project it's always a good idea to keep things consistent. So if you're using
DTOs and Actions for 30 of your models, but writing queries in the controllers for 15 of your models, it may
not be the best approach.

The second important point that you may consider: writing the logic in the Controller is not reusable. So if
you write the store method as above, there's no way to implement an import for example. When the client
gives you a big XLS and you have to build the database from it. If you have actions, it's easy to do this.

You may also ask: why use the DTO, why not just pass the request to the action? Generally, I think that it's a
bad idea to pass around an object in your business logic layer that clearly belongs to the HTTP layer. Other
than that the DTO is independent of the context, so once again it can be used for an import. Think about it,
if we use the Request object in the Action, then how do we call it from the import Command?

So in this book, I will stick with Actions and DTOs. I consider them as best practices.

44 / 92
Martin Joo - Test-Driven APIs with Laravel and Pest

Update a department
First, we write the tests:

it('should update a department', function (string $name, string $description)


{
$department = Department!'factory([
'name' !) 'Development',
])!#create();

putJson(route('departments.update', compact('department')), [
'name' !) $name,
'description' !) $description,
])!#assertNoContent();

expect(Department!'find($department!#id))
!#name!#toBe($name)
!#description!#toBe($description);
})!#with([
['name' !) 'Development', 'description' !) 'Updated Description'],
['name' !) 'Development New', 'description' !) 'Updated Description'],
]);

The steps are:

Create a department with the name 'Development'.


Update it with a new name and description.
Check if the database contains the updated model.

In the case of a PUT request, I usually don't have a response body, this is why I query the updated
department from the database. You can see that I specified two datasets for with . In the first one, the
name is the same as the original department's name that was created with the factory. This is important
because the name must be unique, but obviously, when it's an update we want to ignore the current model.
This test case covers that scenario. And I cover it with a test because it needs a special validation rule called
ignore :

45 / 92
Martin Joo - Test-Driven APIs with Laravel and Pest

public function rules()


{
return [
'name' !) [
'required',
'string',
Rule!'unique('departments', 'name')!#ignore($this!#department),
],
'description' !) [
'nullable',
'sometimes',
'string',
],
];
}

If you don't add this to your validation you cannot update a department unless you change its name. Now
let's implement it. Our new action:

class UpdateDepartmentAction
{
public function execute(
Department $department,
DepartmentData $departmentData
): Department {
$department!#name = $departmentData!#name;
$department!#description = $departmentData!#description;
$department!#save();

return $department;
}
}

46 / 92
Martin Joo - Test-Driven APIs with Laravel and Pest

And the updated controller:

class DepartmentController extends Controller


{
public function !*construct(
private readonly CreateDepartmentAction $createDepartment,
private readonly UpdateDepartmentAction $updateDepartment,
) {}

public function store(StoreDepartmentRequest $request)


{
$departmentData = new DepartmentData(!!($request!#validated());
$department = $this!#createDepartment!#execute($departmentData);

return DepartmentResource!'make($department)
!#response()
!#status(Response!'HTTP_CREATED);
}

public function update(


UpdateDepartmentRequest $request,
Department $department
): HttpResponse {
$departmentData = new DepartmentData(!!($request!#validated());
$department = $this!#updateDepartment!#execute($department,
$departmentData);

return response()!#noContent();
}
}

Wait a minute... Everything is duplicated! You're right, so let's refactor it. We have tests for both create and
update, and we found duplicated code. Remember the refactor stage from the TDD chapter?

47 / 92
Martin Joo - Test-Driven APIs with Laravel and Pest

First, we can extract a method from the controller methods to remove duplication:

public function store(UpsertDepartmentRequest $request): JsonResponse


{
return DepartmentResource!'make($this!#upsert($request, new Department()))
!#response()
!#setStatusCode(Response!'HTTP_CREATED);
}

public function update(


UpsertDepartmentRequest $request,
Department $department
): HttpResponse {
$this!#upsert($request, $department);
return response()!#noContent();
}

private function upsert(


UpsertDepartmentRequest $request,
Department $department
): Department {
$departmentData = new DepartmentData(!!($request!#validated());
return $this!#upsertDepartment!#execute($department, $departmentData);
}

Important note: upsert is the only method I use to write in the controllers besides the standard CRUD
methods.

The trick here is that in the store I create an empty Department and pass it to upsert which will pass it to
the action:

48 / 92
Martin Joo - Test-Driven APIs with Laravel and Pest

class UpsertDepartmentAction
{
public function execute(
Department $department,
DepartmentData $departmentData
): Department {
$department!#name = $departmentData!#name;
$department!#description = $departmentData!#description;
$department!#save();

return $department;
}
}

In the action, we use $model->save() so if it exists Eloquent will run an update query, if it doesn't exist it
will run an insert query. Another option would be to use updateOrCreate which looks like:

class UpsertDepartmentAction
{
public function execute(
Department $department,
DepartmentData $departmentData
): Department {
return Department!'updateOrCreate(
[
'id' !) $department!#id,
],
[
'name' !) $departmentData!#name,
'description' !) $departmentData!#description,
],
);
}
}

But in this situation using save is a better solution in my opinion.

49 / 92
Martin Joo - Test-Driven APIs with Laravel and Pest

Get departments
We need to implement two endoints:

GET /api/v1/departments
GET /api/v1/departments/{department}

This is really easy:

public function index(): AnonymousResourceCollection


{
return DepartmentResource!'collection(Department!'all());
}

public function show(Department $department): DepartmentResource


{
return DepartmentResource!'make($department);
}

We don't need anything else.

50 / 92
Martin Joo - Test-Driven APIs with Laravel and Pest

Resources
So far we haven't talked about API resources. Earlier, I told you that there's a great package that helps us
implement JSON API. It's called timacdonald/json-api and the DepartmentResource looks like this:

use TiMacDonald\JsonApi\JsonApiResource;

class DepartmentResource extends JsonApiResource


{
public function toAttributes(Request $request): array
{
return [
'name' !) $this!#name,
'description' !) $this!#description,
];
}

public function toRelationships(Request $request): array


{
return [
'employees' !) fn () !) EmployeeResource!'collection($this!#employees),
];
}

public function toLinks(Request $request): array


{
return [
'self' !) route('departments.show', ['department' !) $this!#uuid]),
];
}
}

51 / 92
Martin Joo - Test-Driven APIs with Laravel and Pest

This produces the following response:

{
"data": {
"id": "e12cea58-d2ec-495a-a542-03fd29872d4c",
"type": "departments",
"attributes": {
"name": "Development",
"description": "Illum accusamus cumque aut modi."
},
"relationships": {},
"meta": {},
"links": {
"self": "http:!"localhost:8000/api/v1/departments/e12cea58-d2ec-495a-
a542-03fd29872d4c"
}
},
"included": []
}

So instead of using the toArray method we have:

toAttributes : responsible for the attributes


toRelationships : responsible for the relations and included . These two keys are tied together.
toLinks : responsible for the links

And as you can see I need to extend the JsonApiResource provided by this package instead of Laravel's
JsonResource .

Important note: In the toRelationships method we need to use callbacks. That's because it's lazily
evaluated. This package handles the include query parameter from the URL. So if the URL looks like this:

GET /departments/e12cea58-d2ec-495a-a542-03fd29872d4c?include=employees

Then it will execute the callback and loads all the employee models. But if the URL looks like this:

GET /departments/e12cea58-d2ec-495a-a542-03fd29872d4c

52 / 92
Martin Joo - Test-Driven APIs with Laravel and Pest

Then it will not execute the callback and employees won't be loaded. This is similar to the whenLoaded
function from Laravel's resource class.

53 / 92
Martin Joo - Test-Driven APIs with Laravel and Pest

Employee API
Payment types
Now we're gonna move on to employees. Before writing the API we need to handle payment types. It's
either salary or hourly rate:

Salary means that we store the yearly salary for an employee, and when it's payday, the application will
create a paycheck with amount = salary / 12.
Hourly rate means that we store the hourly rate of an employee and when it's payday, the app will sum
up the time logs for the current month and create a paycheck with amount = hours worked * hourly
rate.

We can leverage 3 things here:

PHP8.1 enums
Factories
Strategy design pattern

First, I will discuss these concepts one by one, but after that, I put everything together, and hopefully it's
gonna be clear. Let's start with the enum:

namespace App\Enums;

enum PaymentTypes: string


{
case SALARY = 'salary';
case HOURLY_RATE = 'hourlyRate';
}

This is a backed enum with the type of string. As you can see each case has a string value. This is good
because it can be used in validations for example, and we also want to store these values for
employees.payment_type column (later on that).

An enum can be used like this:

$type = PaymentTypes!'from('salary');
$type!#value !!% 'salary';

The from is a factory function provided by PHP. It can be used to create an enum from a scalar value, and
it's possible because the PaymentTypes is a backed enum with string. The value property will return the
value you specify after a case , so salary or hourlyRate in this case.

Later this enum will be used in the employee API's validation:

54 / 92
Martin Joo - Test-Driven APIs with Laravel and Pest

'paymentType' !) [
'required',
new Enum(PaymentTypes!'class),
],

This validation will only accept salary or hourlyRate in the paymentType key.

Moving on to the strategy pattern. The employees table has a column called payment_type , each payment
type will require different logic when paying an employee, right? Typically you can write something like this:

public function payEmployee(Employee $employee): void


{
if ($employee!#payment_type !!% 'salary')
{
!" Calculate the amount based on yearly salary
}

if ($employee!#payment_type !!% 'hourlyRate')


{
!" Calculate the amount based on time logs for the current month
}
}

And everywhere in the application where you want to do something based on the payment type, you must
check these values. Now imagine a bigger, more complex enterprise application where maybe you have:

Invoices with 8 different statuses (Draft, Pending, etc)


2 different types (debit, credit)
Products with 5 types
Orders with statuses

These statuses, types can easily become a nightmare and cause tons of if-else statements. And oftentimes
these if statements will nest like hell because a pending debit invoice needs different logic than a paid debit
invoice. Fortunately, the strategy pattern can help in these situations.

Instead of writing if statements for the different values, we can create a separate class for each one:

55 / 92
Martin Joo - Test-Driven APIs with Laravel and Pest

abstract class PaymentType


{
public function !*construct(protected readonly Employee $employee)
{
}

abstract public function monthlyAmount(): int;


abstract public function type(): string;
abstract public function amount(): int;
}

class Salary extends PaymentType


{
public function monthlyAmount(): int
{
!" Calculate the monthly salary
}

public function type(): string


{
return PaymentTypes!'SALARY!#value;
}

public function amount(): int


{
return $this!#employee!#salary;
}
}

class HourlyRate extends PaymentType


{
public function monthlyAmount(): int
{
!" Calculate the amount based on time logs for the current month
}

56 / 92
Martin Joo - Test-Driven APIs with Laravel and Pest

public function type(): string


{
return PaymentTypes!'HOURLY_RATE!#value;
}

public function amount(): int


{
return $this!#employee!#hourly_rate;
}
}

There is a base class that defines the common interface. In this case, each payment type has a
monthlyAmount method that will return the amount that needs to be paid for the employee. The Salary
subclass will calculate it from the yearly salary, while the HourlyRate subclass will work with the time logs.
After this refactor this is how the payEmployee method looks like:

public function payEmployee(Employee $employee): void


{
$amount = $employee!#payment_type!#monthlyAmount();

!" Create the paycheck with this amount


}

Now, you don't have to use if-else statements anymore! You have a separate class for each type:

Salary: only cares about employees with salary


HourlyRate: only cares about employees with an hourly rate

The strategy pattern brings one more important benefit. If you need to handle more payment types, you
don't have to change your code in multiple places. The only thing you need to do is to add a new class! For
example:

!" Same as before


class Salary extends PaymentType
{
public function monthlyAmount(): int
{
!" Calculate the monthly salary
}

57 / 92
Martin Joo - Test-Driven APIs with Laravel and Pest

!" Same as before


class HourlyRate extends PaymentType
{
public function monthlyAmount(): int
{
!" Calculate the amount based on time logs for the current month
}
}

class Commission extends PaymentType


{
public function monthlyAmount(): int
{
!" Calculate the amount based on sales volume
}
}

And you don't need to change the payEmployee method or any other function where you care about
payment types.

This is a huge benefit, especially in a large application!

In the examples above I assumed that $employee->payment_type is not a string anymore, but a
PaymentType instance. You can achieve this by using an attribute accessor in the Employee model:

public function getPaymentTypeAttribute(): PaymentType


{
return PaymentTypes!'from($this!#original['payment_type'])
!#makePaymentType($this);
}

Please note that the accessor has the same name as the database column, payment_type , so I need to use
the $this->original['payment_type'] . I cannot use $this->payment_tyoe because it will call the
accessor again and it's an infinite loop. I make use of the enum here, and there's a new method called
makePaymentType . This is a factory inside the enum that will create either a Salary or HourlyRate
instance based on the employee:

58 / 92
Martin Joo - Test-Driven APIs with Laravel and Pest

public function makePaymentType(Employee $employee): PaymentType


{
return match ($this) {
self!'SALARY !) new Salary($employee),
self!'HOURLY_RATE !) new HourlyRate($employee),
};
}

As you can see an enum can be used in a match statement. It basically behaves like a factory method. So, by
using

Enums
Strategy
Factory

We can eliminate a lot of if-else statements from the code!

Create / Update an employee


When creating an employee the request needs to have:

A unique and valid e-mail address

A payment type that is salary or hourlyRate

If it's salary then it expects a number in the salary key


If it's hourlyRate then it expects a number in the hourlyRate key

These are important rules so it's a good idea to cover them with tests:

59 / 92
Martin Joo - Test-Driven APIs with Laravel and Pest

it('should return 422 if email is invalid', function (?string $email) {


Employee!'factory([
'email' !) '[email protected]',
])!#create();

postJson(route('employees.store'), [
'fullName' !) 'Test Employee',
'email' !) $email,
'departmentId' !) Department!'factory()!#create()!#uuid,
'jobTitle' !) 'BE Developer',
'paymentType' !) 'salary',
'salary' !) 75000 * 100,
])!#assertInvalid();
})!#with([
'[email protected]',
'invalid',
null,
'',
]);

it('should return 422 if payment type is invalid', function () {


postJson(route('employees.store'), [
'fullName' !) 'Test Employee',
'email' !) '[email protected]',
'departmentId' !) Department!'factory()!#create()!#uuid,
'jobTitle' !) 'BE Developer',
'paymentType' !) 'invalid',
'salary' !) 75000 * 100,
])!#assertInvalid();
});

it('should return 422 if salary or hourly rate missing', function (string


$paymentType, ?int $salary, ?int $hourlyRate) {
postJson(route('employees.store'), [
'fullName' !) 'Test Employee',
'email' !) '[email protected]',

60 / 92
Martin Joo - Test-Driven APIs with Laravel and Pest

'departmentId' !) Department!'factory()!#create()!#uuid,
'jobTitle' !) 'BE Developer',
'paymentType' !) $paymentType,
'salary' !) $salary,
'hourlyRate' !) $hourlyRate,
])!#assertInvalid();
})!#with([
['paymentType' !) 'salary', 'salary' !) null, 'hourlyRate' !) 30 * 100],
['paymentType' !) 'salary', 'salary' !) 0, 'hourlyRate' !) null],
['paymentType' !) 'hourlyRate', 'salary' !) 75000 * 100, 'hourlyRate' !)
null],
['paymentType' !) 'hourlyRate', 'salary' !) null, 'hourlyRate' !) 0],
]);

Some important notes:

The API expects UUIDs, this is why the departmentId key contains UUID.
The API expects the amounts in cents. For readability I used 75000 * 100 which means $75000.
These tests take advantage of Pest's with the same we used them before.

So this is the 'unhappy' path. To make these tests pass we only need a request with the validation rules and
an empty Controller that uses this request.

class UpsertEmployeeRequest extends FormRequest


{
public function getDepartment(): Department
{
return Department!'where('uuid', $this!#departmentId)!#firstOrFail();
}

public function rules()


{
return [
'fullName' !) ['required', 'string'],
'email' !) [
'required',
'email',
Rule!'unique('employees', 'email')!#ignore($this!#employee),
],

61 / 92
Martin Joo - Test-Driven APIs with Laravel and Pest

'departmentId' !) ['required', 'string', 'exists:departments,uuid'],


'jobTitle' !) ['required', 'string'],
'paymentType' !) [
'required',
new Enum(PaymentTypes!'class),
],
'salary' !) ['nullable', 'sometimes', 'integer'],
'hourlyRate' !) ['nullable', 'sometimes', 'integer'],
];
}
}

We'll talk about the salary and hourlyRate validation later. As earlier I used the name upsert because I
know that it's gonna be used in both the POST and PUT API. Some important notes:

I used the ignore rule again (as earlier).


Notice the exists:departments,uuid . It's important to use the uuid column!
And we use an enum validation as discussed before. This will only accept salary and hourlyRate
I often write very basic getter methods in the request classes. It helps simplify the Controller.

After that, the validation tests will pass, so it's time to cover the 'happy' path:

62 / 92
Martin Joo - Test-Driven APIs with Laravel and Pest

it('should store an employee with payment type salary', function () {


$employee = postJson(route('employees.store'), [
'fullName' !) 'John Doe',
'email' !) '[email protected]',
'departmentId' !) Department!'factory()!#create()!#uuid,
'jobTitle' !) 'BE Developer',
'paymentType' !) 'salary',
'salary' !) 75000 * 100,
])!#json('data');

expect($employee)
!#attributes!#fullName!#toBe('John Doe')
!#attributes!#email!#toBe('[email protected]')
!#attributes!#jobTitle!#toBe('BE Developer')
!#attributes!#payment!#type!#toBe('salary')
!#attributes!#payment!#amount!#cents!#toBe(75000 * 100)
!#attributes!#payment!#amount!#dollars!#toBe('$75,000.00');
});

it('should store an employee with payment type hourly rate', function () {


$employee = postJson(route('employees.store'), [
'fullName' !) 'John Doe',
'email' !) '[email protected]',
'departmentId' !) Department!'factory()!#create()!#uuid,
'jobTitle' !) 'BE Developer',
'paymentType' !) 'hourlyRate',
'hourlyRate' !) 30 * 100,
])!#json('data');

expect($employee)
!#attributes!#fullName!#toBe('John Doe')
!#attributes!#email!#toBe('[email protected]')
!#attributes!#jobTitle!#toBe('BE Developer')
!#attributes!#payment!#type!#toBe('hourlyRate')
!#attributes!#payment!#amount!#cents!#toBe(30 * 100)
!#attributes!#payment!#amount!#dollars!#toBe('$30.00');

63 / 92
Martin Joo - Test-Driven APIs with Laravel and Pest

});

Two tests for two use-cases: creating an employee with salary, and creating an employee with an hourly
rate. Later I'll show you the resources, but as you can see it has a payment object that looks like this:

{
"payment": {
"type": "hourlyRate",
"amount": {
"cents": 3000,
"dollars": "$30.00"
}
}
}

A nice, nested object that contains both the raw data and the formatted as well.

We'll use DTOs just as before, but the EmployeeData is a little bit more complicated than the
DepartmentData was:

class EmployeeData
{
public function !*construct(
public readonly string $fullName,
public readonly string $email,
public readonly Department $department,
public readonly string $jobTitle,
public readonly string $paymentType,
public readonly ?int $salary,
public readonly ?int $hourlyRate,
) {}

public static function fromRequest(UpsertEmployeeRequest $request): self


{
return new static(
$request!#fullName,
$request!#email,
$request!#getDepartment(),

64 / 92
Martin Joo - Test-Driven APIs with Laravel and Pest

$request!#jobTitle,
$request!#paymentType,
$request!#salary,
$request!#hourlyRate,
);
}
}

If you remember the DepartmentData only has a constructor and is instantiated by:

new DepartmentData(!!($request!#validated());

However, the EmployeeData contains some nullable fields and it also has a department. In this case, it's
much easier to write a factory function that takes a request. This is where we use the getDepartment from
the request. I consider it best practice to use models in a DTO instead of IDs. In this case, I used a
Department instance instead of an ID.

In the Controller I used exactly the approach as before:

class EmployeeController extends Controller


{
public function !*construct(
private readonly UpsertEmployeeAction $upsertEmployee
) {}

public function store(UpsertEmployeeRequest $request): JsonResponse


{
return EmployeeResource!'make($this!#upsert($request, new Employee()))
!#response()
!#setStatusCode(Response!'HTTP_CREATED);
}

public function update(


UpsertEmployeeRequest $request,
Employee $employee
): HttpResponse {
$employee = $this!#upsert($request, $employee);
return response()!#noContent();

65 / 92
Martin Joo - Test-Driven APIs with Laravel and Pest

private function upsert(


UpsertEmployeeRequest $request,
Employee $employee
): Employee {
$employeeData = EmployeeData!'fromRequest($request);
return $this!#upsertEmployee!#execute($employee, $employeeData);
}
}

The only difference, in this case, the upsert method uses the fromRequest method from the DTO to
create a new instance.

66 / 92
Martin Joo - Test-Driven APIs with Laravel and Pest

Now, let's see the Action:

class UpsertEmployeeAction
{
/**
* @throws ValidationException
!$
public function execute(
Employee $employee,
EmployeeData $employeeData
): Employee {
$this!#validate($employeeData);

$employee!#full_name = $employeeData!#fullName;
$employee!#email = $employeeData!#email;
$employee!#department_id = $employeeData!#department!#id;
$employee!#job_title = $employeeData!#jobTitle;
$employee!#payment_type = $employeeData!#paymentType;
$employee!#salary = $employeeData!#salary;
$employee!#hourly_rate = $employeeData!#hourlyRate;
$employee!#save();

return $employee;
}

67 / 92
Martin Joo - Test-Driven APIs with Laravel and Pest

Really simple. The only new thing is the validate method:

/**
* @throws ValidationException
!$
private function validate(EmployeeData $employeeData): void
{
$rules = [
$employeeData!#paymentType !) [
'required',
'numeric',
Rule!'notIn([0]),
],
];

$validator = Validator!'make([
'paymentType' !) $employeeData!#paymentType,
'salary' !) $employeeData!#salary,
'hourlyRate' !) $employeeData!#hourlyRate,
], $rules);

$validator!#validate();
}

I decided to validate the salary and hourlyRate by hand instead of using the request. The reasons:

It seemed too complicated in the request with required_if


But more importantly, if we want to create employees via an import or any other command, we need
this validation to be in the Action! Otherwise, it will store invalid data.

68 / 92
Martin Joo - Test-Driven APIs with Laravel and Pest

Get employees
This endpoint will return all the employees, and we also want some filtering based on:

Full name
Job title
Email
Department's name

So we accept requests like:

# Returns employees where the email contains "[email protected]"


GET /api/v1/employees?filter[email][email protected]

# Returns employees where the full_name contains "freek" AND the job_title
contains "developer"
GET /api/v1/employees?filter[full_name]=freek&filter[job_title]=developer

# Returns employees where the name of the department contains "development"


GET /api/v1/employees?filter[department.name]=development

# Returns every employee with department. No N+1 query problem of course.


GET /api/v1/employees?include=department

Fortunately, this is very easy to achieve with Spatie's laravel-query-builder:

class EmployeeController extends Controller


{
public function index(GetEmployeesRequest $request)
{
$employees = QueryBuilder!'for(Employee!'class)
!#allowedFilters(
['full_name', 'job_title', 'email', 'department.name']
)
!#allowedIncludes(['department'])
!#get();

return EmployeeResource!'collection($employees);
}

69 / 92
Martin Joo - Test-Driven APIs with Laravel and Pest

As you can see we only need to use two methods:

allowedFilters : this array defines what columns can be used in the request URL under the filter
key.
allowedIncludes : this array defines what relationships can be included by the client.

And that's it! We have a fully functioning filter in our API. This package can do much more so check out the
documentation!

Get department employees


Earlier I talked about nested resources. Imagine there's a page for every department and we need to list the
employees in a department. There are two solutions:

GET /api/v1/employees?filter[department_id]=e12cea58-d2ec-495a-a542-
03fd29872d4c

GET /api/v1/departments/e12cea58-d2ec-495a-a542-03fd29872d4c/employees

I almost always choose the second one here. There are a few reasons:

It's a "first-class citizen". This is a separate endpoint, with its own controller, which is in my opinion
always a better option than having a (probably) undocumented filter in some endpoint.
It feels more like a REST API and just looks better for me.

We also need to implement filters in this endpoint:

GET /api/v1/departments/e12cea58-d2ec-495a-a542-03fd29872d4c/employees?
filter[full_name]=john

The routes looks like this:

Route!'get(
'departments/{department}/employees',
[DepartmentEmployeeController!'class, 'index']
)!#name('department-employees.index');

70 / 92
Martin Joo - Test-Driven APIs with Laravel and Pest

Now, let's write a basic test:

it('should return all employees for a department', function () {


$development = Department!'factory(['name' !) 'Development'])!#create();
$marketing = Department!'factory(['name' !) 'Marketing'])!#create();

$developers = Employee!'factory([
'department_id' !) $development!#id,
])!#count(5)!#create();

Employee!'factory([
'department_id' !) $marketing!#id,
])!#count(2)!#create();

$employees = getJson(route('department-employees.index', ['department' !)


$development]))
!#json('data');

expect($employees)!#toHaveCount(5);
expect($employees)
!#each(fn ($employee) !) $employee!#id!#toBeIn($developers-
>pluck('uuid')));
});

Step by step:

Create two departments.


Create employees for these departments. There are 5 developers.
Call the API with the development department.
After that, we expect that the response contains the 5 developers.

I use two useful Pest functions here:

each : works the same as the Collection's each function.


toBeIn : it checks that a given value is in a collection or array.

With these two functions, we can check that all employees from the response are developers.

71 / 92
Martin Joo - Test-Driven APIs with Laravel and Pest

Next, we can test the filters:

it('should filter employees', function () {


$development = Department!'factory(['name' !) 'Development'])!#create();
$marketing = Department!'factory(['name' !) 'Marketing'])!#create();

Employee!'factory([
'department_id' !) $development!#id,
])!#count(4)!#create();

$developer = Employee!'factory([
'full_name' !) 'Test John Doe',
'department_id' !) $development!#id,
])!#create();

Employee!'factory([
'department_id' !) $marketing!#id,
])!#count(2)!#create();

$employees = getJson(
route('department-employees.index', [
'department' !) $development,
'filter' !) [
'full_name' !) 'Test',
]
]))
!#json('data');

expect($employees)!#toHaveCount(1);
expect($employees[0])!#id!#toBe($developer!#uuid);
});

The setup is very similar, but here we have the $developer , which contains the employee we will search
for.

When I have nested resources I almost always create separate controllers. In this case, it's the
DepartmentEmployeeController :

72 / 92
Martin Joo - Test-Driven APIs with Laravel and Pest

class DepartmentEmployeeController extends Controller


{
public function index(
GetDepartmentEmployeesRequest $request,
Department $department
) {
$employees = QueryBuilder!'for(Employee!'class)
!#allowedFilters(['full_name', 'job_title', 'email'])
!#whereBelongsTo($department)
!#get();

return EmployeeResource!'collection($employees);
}
}

As you can see we can chain any other Eloquent method to the Spatie query builder. In this case, I used the
whereBelongsTo which is the same as:

!#where('department_id', $department!#id);

By the way, in most cases, I don't try to generalize these query builder configs and I almost always write
them in the controllers. That's because it's unique for this particular endpoint. But if you find yourself
writing the filters over and over again, maybe you should create an allowedFilters property or function
for your models.

73 / 92
Martin Joo - Test-Driven APIs with Laravel and Pest

Resources and value objects


As I wrote earlier we want to have this JSON data structure for an employee:

{
"payment": {
"type": "hourlyRate",
"amount": {
"cents": 3000,
"dollars": "$30.00"
}
}
}

We also need this structure for the paychecks later. So it's a good idea to create an object that holds money
values:

74 / 92
Martin Joo - Test-Driven APIs with Laravel and Pest

namespace App\ValueObjects;

class Money
{
public function !*construct(private readonly int $valueInCents)
{
}

public static function from(int $valueInCents): self


{
return new static($valueInCents);
}

public function toDollars(): string


{
return '$' . number_format($this!#valueInCents / 100, 2);
}

public function toCents(): int


{
return $this!#valueInCents;
}
}

This is a value object I talked about earlier. As you can see it's really just a class that holds some value. It
holds an integer value (the amount in cents) and can format it as dollars value and as cents value. That's it.

75 / 92
Martin Joo - Test-Driven APIs with Laravel and Pest

The benefits:

Single source of truth: If you need to format the values differently you only need to change one class.
Cents and dollars values live together in one class. So cohesive data lives together. That usually makes
most applications easier to maintain.
It's type hinted.

Now, we can write the resource using this value object:

class EmployeeResource extends JsonApiResource


{
public function toAttributes(Request $request): array
{
return [
'fullName' !) $this!#full_name,
'email' !) $this!#email,
'jobTitle' !) $this!#job_title,
'payment' !) [
'type' !) $this!#payment_type!#type(),
'amount' !) [
'cents' !) Money!'from($this!#payment_type!#amount())!#toCents(),
'dollars' !) Money!'from($this!#payment_type!#amount())-
>toDollars(),
],
],
];
}
}

76 / 92
Martin Joo - Test-Driven APIs with Laravel and Pest

But we know that a paycheck will contain the same data structure, so we can create an Amount value
object:

class Amount
{
public function !*construct(
private readonly Money $cents,
private readonly Money $dollars
) {}

public static function from(int $valueInCents): self


{
return new static(
Money!'from($valueInCents),
Money!'from($valueInCents),
);
}

/**
* @return array{cents: int, dollars: string}
!$
public function toArray(): array
{
return [
'cents' !) $this!#cents!#toCents(),
'dollars' !) $this!#dollars!#toDollars(),
];
}
}

This class does not contain any real logic, it just holds two Money objects. One for the cents value and one
for the dollars value.

77 / 92
Martin Joo - Test-Driven APIs with Laravel and Pest

This way we can simplify the resource:

class EmployeeResource extends JsonApiResource


{
public function toAttributes(Request $request): array
{
return [
'fullName' !) $this!#full_name,
'email' !) $this!#email,
'jobTitle' !) $this!#job_title,
'payment' !) [
'type' !) $this!#payment_type!#type(),
'amount' !) Amount!'from($this!#payment_type!#amount())!#toArray(),
],
];
}
}

That's better! Later we will reuse this class.

78 / 92
Martin Joo - Test-Driven APIs with Laravel and Pest

Payday API
Create paychecks
The payday API will create the paychecks for employees based on their salary or hourly rate. We can start
with some tests:

it('should create paychecks for salary employees', function () {


$employees = Employee!'factory()
!#count(2)
!#sequence(
[
'salary' !) 50000 * 100,
'payment_type' !) PaymentTypes!'SALARY!#value
],
[
'salary' !) 70000 * 100,
'payment_type' !) PaymentTypes!'SALARY!#value
],
)
!#create();

postJson(route('payday.store'))
!#assertNoContent();

$this!#assertDatabaseHas('paychecks', [
'employee_id' !) $employees[0]!#id,
'net_amount' !) 416666,
]);
$this!#assertDatabaseHas('paychecks', [
'employee_id' !) $employees[1]!#id,
'net_amount' !) 583333,
]);
});

79 / 92
Martin Joo - Test-Driven APIs with Laravel and Pest

This test makes sure that the API will create a paycheck for employees with yearly salaries:

It creates two employees. I used the sequence method from Factory . It will create 2 employees with
the given attributes.

Calls the API.

After that, it checks that there are two records in the paychecks table with the right amount.

The salary column in the employees table contains yearly salary, like 50000.
The net_amount column in the paychecks table contains the monthly amount.
In the first case we expect 50000 / 12 so $4166.66, in the second case we expect 70000 / 12 so
$5833.33

It's really easy to implement this feature. If you remember we have a Salary class that has a
monthlyAmount method. This method will return the monthly amount for a salaried employee, so yearly
salary / 12:

80 / 92
Martin Joo - Test-Driven APIs with Laravel and Pest

class Salary extends PaymentType


{
public function !*construct(Employee $employee)
{
throw_if(
$employee!#salary !!% null,
new RuntimeException('Hourly rate cannot be null')
);

parent!'!*construct($employee);
}

public function type(): string


{
return PaymentTypes!'SALARY!#value;
}

public function amount(): int


{
return $this!#employee!#salary;
}

public function monthlyAmount(): int


{
return $this!#employee!#salary / 12;
}
}

After we have this logic, we can use it in the PaydayAction class:

81 / 92
Martin Joo - Test-Driven APIs with Laravel and Pest

class PaydayAction
{
public function execute(): void
{
foreach (Employee!'all() as $employee) {
$employee!#paychecks()!#create([
'net_amount' !) $employee!#payment_type!#monthlyAmount(),
'payed_at' !) now(),
]);
}
}
}

It goes through all employees, calculates the monthly amount from the PaymentType class and creates a
paycheck with the current date. This class makes the tests pass. As you can see this action (and also the API)
will create paychecks for the current month.

The test for employees with hourly rate is a little bit more complicated:

82 / 92
Martin Joo - Test-Driven APIs with Laravel and Pest

it('should create paychecks for hourly rate employees', function () {


$this!#travelTo(Carbon!'parse('2022-02-10'), function () {
$employee = Employee!'factory([
'hourly_rate' !) 10 * 100,
'payment_type' !) PaymentTypes!'HOURLY_RATE!#value,
])!#create();

$dayBeforeYesterday = now()!#subDays(2);
$yesterday = now()!#subDay();
$today = now();

Timelog!'factory()
!#count(3)
!#sequence(
[
'employee_id' !) $employee,
'minutes' !) 90,
'started_at' !) $dayBeforeYesterday,
'stopped_at' !) $dayBeforeYesterday!#copy()!#addMinutes(90)
],
[
'employee_id' !) $employee,
'minutes' !) 15,
'started_at' !) $yesterday,
'stopped_at' !) $yesterday!#copy()!#addMinutes(15)
],
[
'employee_id' !) $employee,
'minutes' !) 51,
'started_at' !) $today,
'stopped_at' !) $today!#copy()!#addMinutes(51)
],
)
!#create();

postJson(route('payday.store'))

83 / 92
Martin Joo - Test-Driven APIs with Laravel and Pest

!#assertNoContent();

$this!#assertDatabaseHas('paychecks', [
'employee_id' !) $employee!#id,
'net_amount' !) 30 * 100,
]);
});
});

When paying employees with an hourly rate, we need to query time logs that were created in the current
month. In order to ensure that all time logs are created in the same month, we need to time travel.

$this!#travelTo(Carbon!'parse('2022-02-10'), function () { !+ !!( !$ });

This line will "lock" the time to 2022-01-10. So if I call now() in the closure, it will return 2022-02-10. Under
the hood, this function uses:

Carbon!'setTestNow('2022-02-10');

This way, I can create time_logs for February and when I call the API (which uses now() in the Action) it
will query time_logs from February.

So in the test, I create three time_logs that add up to 156 minutes which is 2.6 hours. When paying
employees with an hourly rate we need to round these numbers, so instead of 2.6 hours, we create a
paycheck for 3 hours.

One more caveat: I use $today->copy()->addMinutes() because addMinutes will mutate the original
Carbon object, so if I don't add copy it will add minutes to the started_at date as well.

Now, let's implement this query in the HourlyRate class:

84 / 92
Martin Joo - Test-Driven APIs with Laravel and Pest

class HourlyRate extends PaymentType


{
public function !*construct(Employee $employee)
{
throw_if(
$employee!#hourly_rate !!% null,
new RuntimeException('Hourly rate cannot be null')
);

parent!'!*construct($employee);
}

public function type(): string


{
return PaymentTypes!'HOURLY_RATE!#value;
}

public function amount(): int


{
return $this!#employee!#hourly_rate;
}

public function monthlyAmount(): int


{
$hoursWorked = Timelog!'query()
!#whereBetween('stopped_at', [
now()!#startOfMonth(),
now()!#endOfMonth()
])
!#sum('minutes') / 60;

return round($hoursWorked) * $this!#employee!#hourly_rate;


}
}

85 / 92
Martin Joo - Test-Driven APIs with Laravel and Pest

It's not that complicated:

It queries every Timelog entry where the stopped_at date is in the current month.
It sums the value of minutes .

This function will make the test passes. There is another test case:

it('should create paychecks for hourly rate employees only for current
month', function () {
$this!#travelTo(Carbon!'parse('2022-02-10'), function () {
$employee = Employee!'factory([
'hourly_rate' !) 10 * 100,
'payment_type' !) PaymentTypes!'HOURLY_RATE!#value,
])!#create();

Timelog!'factory()
!#count(2)
!#sequence(
[
'employee_id' !) $employee,
'minutes' !) 60,
'started_at' !) now()!#subMonth(),
'stopped_at' !) now()!#subMonth()!#addMinutes(60)
],
[
'employee_id' !) $employee,
'minutes' !) 60,
'started_at' !) now(),
'stopped_at' !) now()!#addMinutes(60)
],
)
!#create();

postJson(route('payday.store'))
!#assertNoContent();

$this!#assertDatabaseHas('paychecks', [
'employee_id' !) $employee!#id,
'net_amount' !) 10 * 100,

86 / 92
Martin Joo - Test-Driven APIs with Laravel and Pest

]);
});
});

This test makes sure that we only create paychecks for the current month. The first Timelog is due at the
previous month, so after calling the payday endpoint we want to see a paycheck for 1 hour, not 2.

Also, there's an edge-case when it comes to paying hourly rate employees. It's possible that an employee
has no time logs for the current month. Right now, the PaydayAction will create a paycheck with
net_amount 0 in this case. So let's fix this.

First, the test:

it('should not create paychecks for hourly rate employees without time logs',
function () {
$this!#travelTo(Carbon!'parse('2022-02-10'), function () {
Employee!'factory([
'hourly_rate' !) 10 * 100,
'payment_type' !) PaymentTypes!'HOURLY_RATE!#value,
])!#create();

postJson(route('payday.store'))
!#assertNoContent();

$this!#assertDatabaseCount('paychecks', 0);
});
});

87 / 92
Martin Joo - Test-Driven APIs with Laravel and Pest

There are no time logs so we expect the paychecks table to be empty. Of course, it will fail, we need to
handle this in the PaydayAction :

class PaydayAction
{
public function execute(): void
{
foreach (Employee!'all() as $employee) {
$amount = $employee!#payment_type!#monthlyAmount();
if ($amount !!% 0) {
continue;
}

$employee!#paychecks()!#create([
'net_amount' !) $amount,
'payed_at' !) now(),
]);
}
}
}

If the monthly amount is zero, it will not create a paycheck.

The last we can add to this function is transactions. In my opinion, it's important, because this method will
create hundreds or thousands of paychecks, so if something goes wrong it's a good idea to rollback the
database:

88 / 92
Martin Joo - Test-Driven APIs with Laravel and Pest

class PaydayAction
{
public function execute(): void
{
DB!'transaction(function () {
foreach (Employee!'all() as $employee) {
$amount = $employee!#payment_type!#monthlyAmount();
if ($amount !!% 0) {
continue;
}

$employee!#paychecks()!#create([
'net_amount' !) $amount,
'payed_at' !) now(),
]);
}
});
}
}

Laravel makes it easy by using the DB::transaction . If an exception is thrown in the callback it will
rollback the database. If everything goes well it will commit .

This is one of the main advantages of using the strategy pattern: the 'client' code remains really simple. No
complicated if else statement! This small function handles both salaried and hourly rate employees. If I
need to add employees with a commission, for example, I don't even need to touch it! I just add a new
PaymentType class and a new case to the enum.

89 / 92
Martin Joo - Test-Driven APIs with Laravel and Pest

Get paychecks for an employee


Now that we have the payday API end paychecks, we can create the last API that will return paychecks for an
employee. It's another nested recourse:

GET /api/v1/employees/a3b5ce95-c9c8-40f7-b8d7-06133c768a92/paychecks

The controller is called EmployeePaycheckController and is really simple:

class EmployeePaycheckController extends Controller


{
public function index(Employee $employee)
{
return PaycheckResource!'collection(
$employee!#paychecks()!#latest()!#get()
);
}
}

We can reuse the Amount value object in the resource:

90 / 92
Martin Joo - Test-Driven APIs with Laravel and Pest

class PaycheckResource extends JsonApiResource


{
public function toAttributes(Request $request): array
{
return [
'payedAt' !) $this!#payed_at!#format('Y-m-d'),
'netAmount' !) Amount!'from($this!#net_amount)!#toArray(),
];
}

public function toRelationships(Request $request): array


{
return [
'employee' !) fn () !) new EmployeeResource($this!#employee),
];
}
}

And that's it, the application is complete!

91 / 92
Martin Joo - Test-Driven APIs with Laravel and Pest

Thank you
Thank you very much for reading my book! I hope you have learned a few cool techniques.

By the way, 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.

92 / 92

You might also like