30 Days of Automated Testing:Using PHPUnit【D07】

API Testing

WilliamP
5 min readJan 22, 2023

In the previous articles, we introduced the 3A principle of testing and also many assertion functions, today let’s practice it!

In the past, the most commonly used automated testing subject was probably APIs, and the front-end and back-end separation is also a common pattern in the current web development community, so let’s practice with API testing!

Verifying HTTP Status Code

HTTP Status Code verification is probably the most common API testing scenario. Some usage has been shown in the previous Assertion function examples, but let’s practice it more completely this time.

Note that the following tests will involve the database, so please check the database connection settings in phpunit.xml again. Refer to the content of Day 6 article for details.

It’s worth mentioning that, in order to make it easy to access the route path, the route is usually named when setting up the route.

Case 1:200 — Requesting User Data

  • routes/api.php
<?php

use App\Models\User;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Route;

Route::get('/users', function (Request $request) {
$users = User::all();

return response()->json([
'users' => $users,
]);
})->name('get.user.list');
  • tests/Feature/HttpStatusCodeTest.php
<?php

namespace Tests\Feature;

use Illuminate\Foundation\Testing\RefreshDatabase;
use Tests\TestCase;

class HttpStatusCodeTest extends TestCase
{
use RefreshDatabase;

public function testCanGetUserListWhenEmpty()
{
$response = $this->get(route('get.user.list'));
$response->assertOk();

// It can also be written as the following chained notation
$this->get(route('get.user.list'))
->assertOk();
}
}

In the above code, the users table does not have any User data, but the endpoint for getting the list of Users should still respond normally. Through the above test code, it can verify whether this behavior is working properly.

Case 2:404 — Requesting Non-Existent User Data

  • routes/api.php
<?php

use App\Models\User;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Route;

Route::get('/users/{id}', function (Request $request, $id) {
$user = User::find($id);

if (empty($user)) {
return response()->json([
'error' => 'Not Found',
], 404);
}

return response()->json([
'user' => $user,
]);
})->where('id', '[0-9]?')->name('get.user');
  • tests/Feature/HttpStatusCodeTest.php
<?php

namespace Tests\Feature;

use Illuminate\Foundation\Testing\RefreshDatabase;
use Tests\TestCase;

class HttpStatusCodeTest extends TestCase
{
use RefreshDatabase;

public function testCanGetNotFoundWhenNoGivenUser()
{
$response = $this->get(route('get.user', ['id' => 1,]));
$response->assertNotFound();

// It can also be written as the following chained notation
$this->get(route('get.user', ['id' => 1,]))
->assertNotFound();
}
}

In the above code, the users table has no User data, so when trying to retrieve a User resource with id=1, it should not be found, which is 404. Through the above test code, we can verify that this behavior is working properly.

Case 3:422 — Request Validation Failed

  • routes/api.php
<?php

use App\Models\User;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Route;

Route::post('/users', function (Request $request) {

$request->validate([
'name' => 'required|string',
'email' => 'required|email',
'password' => 'required|string',
]);

$user = User::create($request->all());

return response()->json([
'user' => $user,
]);
})->name('store.user');
  • tests/Feature/HttpStatusCodeTest.php
<?php

namespace Tests\Feature;

use Illuminate\Foundation\Testing\RefreshDatabase;
use Tests\TestCase;

class HttpStatusCodeTest extends TestCase
{
use RefreshDatabase;

public function testCanResponseUnprocessableWhenInvalidRequest()
{
$response = $this->post(
route('store.user'),
['name' => ''],
['Accept' => 'application/json']
);
$response->assertUnprocessable();

// It can also be written as the following chained notation
$this->post(
route('store.user'),
['name' => ''],
['Accept' => 'application/json']
)
->assertUnprocessable();
}
}

In the above code, we have created a route endpoint for creating User data, and implemented the process for creating data. We have also included logic for validating the request data. In the test case below, we are testing the behavior of “when the request content is missing, a response of 422 Unprocessable Entity should be returned” to verify that it is functioning properly.

Verifying Response JSON

In addition to the HTTP Status Code validation previously introduced, another common aspect that needs to be validated when testing APIs is the content of the response JSON itself. Let’s take a look at some examples.

Case 1:Verifying The Content of The JSON Response

  • routes/api.php
<?php

use Illuminate\Http\Request;
use Illuminate\Support\Facades\Route;

Route::get('/users', function (Request $request) {
// Use fake user date
$users = [
[
'id' => 1,
'name' => 'name1',
'email' => 'user1@email.com',
],
[
'id' => 1,
'name' => 'name1',
'email' => 'user1@email.com',
],
];

return response()->json([
'users' => $users,
]);
})->name('get.user.list');
  • tests/Feature/HttpStatusCodeTest.php
<?php

namespace Tests\Feature;

use Illuminate\Foundation\Testing\RefreshDatabase;
use Tests\TestCase;

class HttpStatusCodeTest extends TestCase
{
use RefreshDatabase;

public function testCanGetUserJson()
{
$response = $this->get(route('get.user.list'));
$response->assertJson([
'users' => [
[
'id' => 1,
'name' => 'name1',
'email' => 'user1@email.com',
],
[
'id' => 1,
'name' => 'name1',
'email' => 'user1@email.com',
],
]
]);

// It can also be written as the following chained notation
$this->get(route('get.user.list'))
->assertJson([
'users' => [
[
'id' => 1,
'name' => 'name1',
'email' => 'user1@email.com',
],
[
'id' => 1,
'name' => 'name1',
'email' => 'user1@email.com',
],
]
]);
}
}

In the above code, we have created an endpoint that retrieves a list of users, and we are validating that the JSON response returned after calling this API matches our expectations.

Case 2:Verifying The Structure of The JSON Response.

  • routes/api.php
<?php

use Illuminate\Http\Request;
use Illuminate\Support\Facades\Route;

Route::get('/users', function (Request $request) {
// Use fake user date
$users = [
[
'id' => 1,
'name' => 'name1',
'email' => 'user1@email.com',
],
[
'id' => 1,
'name' => 'name1',
'email' => 'user1@email.com',
],
];

return response()->json([
'users' => $users,
]);
})->name('get.user.list');
  • tests/Feature/HttpStatusCodeTest.php
<?php

namespace Tests\Feature;

use Illuminate\Foundation\Testing\RefreshDatabase;
use Tests\TestCase;

class HttpStatusCodeTest extends TestCase
{
use RefreshDatabase;

public function testCanGetUserJsonStructure()
{
$response = $this->get(route('get.user.list'));

// '*' represents every array element,
// this notation is equivalent to asserting that 'users' is an array
// and that every element of that array is an object with
// 'id', 'name', and 'email' fields
$response->assertJsonStructure([
'users' => [
'*' => [
'id',
'name',
'email'
],
],
]);

// It can also be written as the following chained notation
$this->get(route('get.user.list'))
->assertJsonStructure([
'users' => [
'*' => [
'id',
'name',
'email'
],
],
]);
}
}

Another common scenario is to only verify the JSON structure, while skipping the validation of JSON keys and values.

In the above, we have introduced the testing of API today.

Practice more and see you in the next article where we will learn how to test databases.

If you liked this article or found it helpful, feel free to give it some claps and follow the author!

Articles of This Series

--

--