For an introduction to testing using API Platform, refer to the Core Testing Documentation, or access the Symfony Testing Guide.
Let’s learn how to use tests with Laravel!
In this article, you’ll learn how to use:
💡 Tip
Pest is built on top of PHPUnit and introduces additional features along with a syntax inspired by Ruby’s RSpec and the Jest testing APIs.
💡 Tip
Even if you are using Pest, you can also use PHPUnit’s assertion API, which can be useful if you’re already familiar with PHPUnit’s assertion API or if you need to perform more complex assertions that aren’t available in Pest’s expectation API. For more information see the Pest Assertion API documentation.
By default, when using Laravel, Pest is pre-configured through the Composer plugin pestphp/pest-plugin
. You can find this plugin listed in the allow-plugins
section of your composer.json
file.
To check the Pest installation, run the following command:
php artisan test
If for some reason, Pest is not installed refer to the Pest Installation Guide.
In that case, you can run Pest using:
./vendor/bin/pest
Using Laravel, you can efficiently test databases by combining seeding with model factories. Model factories allow you to generate large amounts of test data quickly, while seeding ensures your database is pre-populated with the necessary records.
To create a factory for your model, you can use Laravel Artisan command. For example, to create a factory for a Book model, run:
php artisan make:factory BookFactory
For advanced customization and configuration, refer to the Defining model Factories Laravel Guide.
Then, you can now use your factory in tests to quickly generate model instances.
Here’s an example of tests, which use the Factory:
<?php
use App\Models\Book;
use Illuminate\Foundation\Testing\RefreshDatabase;
use ApiPlatform\Laravel\Test\ApiTestAssertionsTrait;
uses(RefreshDatabase::class, ApiTestAssertionsTrait::class);
it('fetches the collection of books')
->test(function () {
// Create 100 books using the factory
Book::factory()->count(100)->create();
// Send a GET request to the collection endpoint
$response = $this->getJson('/api/books');
// Assert that the response is successful (200 OK)
$response->assertStatus(200);
// Check the Content-Type header
$response->assertHeader('Content-Type', 'application/json');
// Assert the returned JSON contains the expected structure using assertJsonContains from the trait
$this->assertJsonContains([
'@context' => '/contexts/Book',
'@id' => '/books',
'@type' => 'Collection',
'totalItems' => 100,
'view' => [
'@id' => '/books?page=1',
'@type' => 'PartialCollectionView',
'first' => '/books?page=1',
'last' => '/books?page=4',
'next' => '/books?page=2',
],
], $response->json());
// Assert that 30 items are returned in the response
$this->assertCount(30, $response->json('data'));
});
it('creates a valid book')
->test(function () {
// Send a POST request to create a book
$response = $this->postJson('/api/books', [
'isbn' => '0099740915',
'title' => 'The Handmaid\'s Tale',
'description' => 'Brilliantly conceived and executed, this powerful evocation of twenty-first century America...',
'author' => 'Margaret Atwood',
'publication_date' => '1985-07-31',
]);
// Assert that the book was created successfully (201)
$response->assertStatus(201);
// Check the Content-Type header
$response->assertHeader('Content-Type', 'application/json');
// Assert the returned JSON contains the expected book information using assertJsonContains
$this->assertJsonContains([
'@context' => '/contexts/Book',
'@type' => 'Book',
'isbn' => '0099740915',
'title' => 'The Handmaid\'s Tale',
'author' => 'Margaret Atwood',
'publication_date' => '1985-07-31',
'reviews' => [],
], $response->json());
// Assert that the URI of the created resource matches the expected format
$this->assertMatchesRegularExpression('~^/api/books/\d+$~', $response->json('@id'));
});
it('creates an invalid book and validates error handling')
->test(function () {
// Send a POST request with invalid data
$response = $this->postJson('/api/books', [
'isbn' => 'invalid',
]);
// Assert that the response status is 422 Unprocessable Entity
$response->assertStatus(422);
// Check the Content-Type header
$response->assertHeader('Content-Type', 'application/json');
// Assert the JSON response contains the validation errors using assertJsonContains
$this->assertJsonContains([
'@context' => '/contexts/ConstraintViolationList',
'@type' => 'ConstraintViolationList',
'title' => 'An error occurred',
'description' => [
'isbn' => 'This value is neither a valid ISBN-10 nor a valid ISBN-13.',
'title' => 'This value should not be blank.',
'description' => 'This value should not be blank.',
'author' => 'This value should not be blank.',
'publication_date' => 'This value should not be null.',
],
], $response->json());
});
it('updates a book')
->test(function () {
// Create a book using the factory
$book = Book::factory()->create(['isbn' => '9781344037075']);
// Get the IRI of the book using getIriFromResource from the trait
$iri = $this->getIriFromResource($book);
// Send a PATCH request to update the book's title
$response = $this->patchJson($iri, [
'title' => 'Updated Title',
]);
// Assert that the response is successful (200 OK)
$response->assertStatus(200);
// Assert the JSON response contains the updated book information using assertJsonContains
$this->assertJsonContains([
'@id' => $iri,
'isbn' => '9781344037075',
'title' => 'Updated Title',
], $response->json());
});
it('deletes a book')
->test(function () {
// Create a book using the factory
$book = Book::factory()->create(['isbn' => '9781344037075']);
// Get the IRI of the book using getIriFromResource from the trait
$iri = $this->getIriFromResource($book);
// Send a DELETE request to remove the book
$response = $this->deleteJson($iri);
// Assert that the response status is 204 No Content
$response->assertStatus(204);
// Assert that the book is no longer in the database
$this->assertDatabaseMissing('books', ['id' => $book->id]);
});
In the example above, the RefreshDatabase Trait is used to ensure that the database is automatically reset between test runs. This guarantees that each test starts with a clean database state, avoiding conflicts from residual data and ensuring test isolation.
This trait is especially useful when testing operations that modify the database, as it rolls back any changes made during the test. As a result, your test environment remains reliable and consistent across multiple test executions.
If everything is working properly, you should see Tests: 5 passed (15 assertions)
.
Your REST API is now properly tested!
Check out the API Test Assertions section to discover the full range of assertions and other features provided by API Platform’s test utilities.
If you want to migrate from PHPUnit to Pest, refer to Migrating from PHPUnit Guide and Installation Guide.
By default, with Laravel, PHPUnit is already a dependency in your project. You may see phpunit/phpunit
in the require-dev
section of your composer.json
.
You can test the PHPUnit installation by running:
./vendor/bin/phpunit --version
If for some reason, PHPUnit is not installed refer to the PHPUnit Installation Guide.
For instructions on generating the factory, please refer to the Generate The Factory section.
Here’s an example of a test class, which use the Factory:
<?php
namespace Tests\Feature;
use App\Models\Book;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Tests\TestCase;
use ApiPlatform\Laravel\Test\ApiTestAssertionsTrait;
class BooksTest extends TestCase
{
use RefreshDatabase, ApiTestAssertionsTrait;
/**
* Test to fetch the collection of books.
*/
public function testGetCollection(): void
{
// Create 100 books using the factory
Book::factory()->count(100)->create();
// Send a GET request to the collection endpoint
$response = $this->getJson('/api/books');
// Assert that the response is successful (200 OK)
$response->assertStatus(200);
// Check the Content-Type header
$response->assertHeader('Content-Type', 'application/json');
// Assert the returned JSON contains the expected structure using assertJsonContains from the trait
$this->assertJsonContains([
'@context' => '/contexts/Book',
'@id' => '/books',
'@type' => 'Collection',
'totalItems' => 100,
'view' => [
'@id' => '/books?page=1',
'@type' => 'PartialCollectionView',
'first' => '/books?page=1',
'last' => '/books?page=4',
'next' => '/books?page=2',
],
], $response->json());
// Assert that 30 items are returned in the response
$this->assertCount(30, $response->json('data'));
}
/**
* Test to create a valid book.
*/
public function testCreateBook(): void
{
// Send a POST request to create a book
$response = $this->postJson('/api/books', [
'isbn' => '0099740915',
'title' => 'The Handmaid\'s Tale',
'description' => 'Brilliantly conceived and executed, this powerful evocation of twenty-first century America...',
'author' => 'Margaret Atwood',
'publication_date' => '1985-07-31',
]);
// Assert that the book was created successfully (201)
$response->assertStatus(201);
// Check the Content-Type header
$response->assertHeader('Content-Type', 'application/json');
// Assert the returned JSON contains the expected book information using assertJsonContains
$this->assertJsonContains([
'@context' => '/contexts/Book',
'@type' => 'Book',
'isbn' => '0099740915',
'title' => 'The Handmaid\'s Tale',
'author' => 'Margaret Atwood',
'publication_date' => '1985-07-31',
'reviews' => [],
], $response->json());
// Assert that the URI of the created resource matches the expected format
$this->assertMatchesRegularExpression('~^/api/books/\d+$~', $response->json('@id'));
}
/**
* Test to create an invalid book and validate error handling.
*/
public function testCreateInvalidBook(): void
{
// Send a POST request with invalid data
$response = $this->postJson('/api/books', [
'isbn' => 'invalid',
]);
// Assert that the response status is 422 Unprocessable Entity
$response->assertStatus(422);
// Check the Content-Type header
$response->assertHeader('Content-Type', 'application/json');
// Assert the JSON response contains the validation errors using assertJsonContains
$this->assertJsonContains([
'@context' => '/contexts/ConstraintViolationList',
'@type' => 'ConstraintViolationList',
'title' => 'An error occurred',
'description' => [
'isbn' => 'This value is neither a valid ISBN-10 nor a valid ISBN-13.',
'title' => 'This value should not be blank.',
'description' => 'This value should not be blank.',
'author' => 'This value should not be blank.',
'publication_date' => 'This value should not be null.',
],
], $response->json());
}
/**
* Test to update a book.
*/
public function testUpdateBook(): void
{
// Create a book using the factory
$book = Book::factory()->create(['isbn' => '9781344037075']);
// Get the IRI of the book using getIriFromResource from the trait
$iri = $this->getIriFromResource($book);
// Send a PATCH request to update the book's title
$response = $this->patchJson($iri, [
'title' => 'Updated Title',
]);
// Assert that the response is successful (200 OK)
$response->assertStatus(200);
// Assert the JSON response contains the updated book information using assertJsonContains
$this->assertJsonContains([
'@id' => $iri,
'isbn' => '9781344037075',
'title' => 'Updated Title',
], $response->json());
}
/**
* Test to delete a book.
*/
public function testDeleteBook(): void
{
// Create a book using the factory
$book = Book::factory()->create(['isbn' => '9781344037075']);
// Get the IRI of the book using getIriFromResource from the trait
$iri = $this->getIriFromResource($book);
// Send a DELETE request to remove the book
$response = $this->deleteJson($iri);
// Assert that the response status is 204 No Content
$response->assertStatus(204);
// Assert that the book is no longer in the database
$this->assertDatabaseMissing('books', ['id' => $book->id]);
}
}
In the example above, the RefreshDatabase Trait is used to ensure that the database is automatically reset between test runs. This guarantees that each test starts with a clean database state, avoiding conflicts from residual data and ensuring test isolation.
This trait is especially useful when testing operations that modify the database, as it rolls back any changes made during the test. As a result, your test environment remains reliable and consistent across multiple test executions.
If everything is working properly, you should see OK (5 tests, 15 assertions)
.
Your REST API is now properly tested!
Check out the API Test Assertions section to discover the full range of assertions and other features provided by API Platform’s test utilities.
In addition to integration tests written using the helpers provided by Pest and PHPUnit, all the classes of your project should be covered by unit tests. To do so, learn how to write unit tests with Pest, PHPUnit and Laravel Creating Tests Guide.
Running your test suite in your CI/CD pipeline is important to ensure good quality and delivery time.
The API Platform distribution is shipped with a GitHub Actions workflow that builds the Docker images, does a smoke test to check that the application’s entrypoint is accessible, and runs PHPUnit.
The API Platform Demo contains a CD workflow that uses the Helm chart provided with the distribution to deploy the app on a Kubernetes cluster.
You may also be interested in these alternative testing tools (not included in the API Platform distribution):
In addition to the built-in ones, API Platform provides convenient PHPUnit assertions dedicated to API testing:
<?php
// api/tests/MyTest.php
namespace App\Tests;
use ApiPlatform\Laravel\Test\ApiTestAssertionsTrait;
use Tests\TestCase;
class MyTest extends TestCase
{
use ApiTestAssertionsTrait;
public function testSomething(): void
{
// $response = $this->get('/');
// Asserts that an array has a specified subset.
$this->assertArraySubset(/* An array or an iterable */);
// Asserts that the returned JSON is a superset of the passed one
$this->assertJsonContains(/* a JSON document as an array or as a string */);
}
}
There is also a method to find the IRI matching a given resource:
<?php
// api/tests/BooksTest.php
namespace App\Tests;
use ApiPlatform\Laravel\Test\ApiTestAssertionsTrait;
use App\Models\Book;
use Tests\TestCase;
class BooksTest extends TestCase
{
use ApiTestAssertionsTrait;
public function testFindBook(): void
{
$book = Book::firstOrFail();
$iri = $this->getIriFromResource($book);
$response = $this->get($iri);
$response->assertStatus(200);
}
}
Made with love by
Les-Tilleuls.coop can help you design and develop your APIs and web projects, and train your teams in API Platform, Symfony, Next.js, Kubernetes and a wide range of other technologies.
Learn more