# Testing the API
Now that you have a functional API, you should write tests to ensure it has no bugs, and to prevent future regressions.
Some would argue that it's even better to [write tests first](https://martinfowler.com/bliki/TestDrivenDevelopment.html).
API Platform provides a set of helpful testing utilities to write unit tests, functional tests, and to create [test fixtures](https://en.wikipedia.org/wiki/Test_fixture#Software).
Let's learn how to use them!

Watch the Tests & Assertions screencast
In this article you'll learn how to use:
* [PHPUnit](https://phpunit.de), a testing framework to cover your classes with unit tests and to write
API-oriented functional tests thanks to its API Platform and [Symfony](https://symfony.com/doc/current/testing.html) integrations.
* [DoctrineFixturesBundle](https://symfony.com/bundles/DoctrineFixturesBundle/current/index.html), a bundle to load data fixtures in the database.
* [Foundry](https://github.com/zenstruck/foundry), an expressive fixtures generator to write data fixtures.
## Creating Data Fixtures
Before creating your functional tests, you will need a dataset to pre-populate your API and be able to test it.
First, install [Foundry](https://github.com/zenstruck/foundry) and [Doctrine/DoctrineFixturesBundle](https://github.com/doctrine/DoctrineFixturesBundle):
```console
docker compose exec php \
composer require --dev foundry orm-fixtures
```
Thanks to Symfony Flex, [DoctrineFixturesBundle](https://github.com/doctrine/DoctrineFixturesBundle) and [Foundry](https://github.com/zenstruck/foundry) are ready to use!
Then, create some factories for [the bookstore API you created in the tutorial](index.md):
```console
docker compose exec php \
bin/console make:factory 'App\Entity\Book'
docker compose exec php \
bin/console make:factory 'App\Entity\Review'
```
Improve the default values:
```php
// src/Factory/BookFactory.php
// ...
protected function getDefaults(): array
{
return [
'author' => self::faker()->name(),
'description' => self::faker()->text(),
'isbn' => self::faker()->isbn13(),
'publication_date' => \DateTimeImmutable::createFromMutable(self::faker()->dateTime()),
'title' => self::faker()->sentence(4),
];
}
```
```php
// src/Factory/ReviewFactory.php
// ...
use function Zenstruck\Foundry\lazy;
// ...
protected function getDefaults(): array
{
return [
'author' => self::faker()->name(),
'body' => self::faker()->text(),
'book' => lazy(fn() => BookFactory::randomOrCreate()),
'publicationDate' => \DateTimeImmutable::createFromMutable(self::faker()->dateTime()),
'rating' => self::faker()->numberBetween(0, 5),
];
}
```
Create some stories:
```console
docker compose exec php \
bin/console make:story 'DefaultBooks'
docker compose exec php \
bin/console make:story 'DefaultReviews'
```
```php
// src/Story/DefaultBooksStory.php
namespace App\Story;
use App\Factory\BookFactory;
use Zenstruck\Foundry\Story;
final class DefaultBooksStory extends Story
{
public function build(): void
{
BookFactory::createMany(100);
}
}
```
```php
// src/Story/DefaultReviewsStory.php
namespace App\Story;
use App\Factory\ReviewFactory;
use Zenstruck\Foundry\Story;
final class DefaultReviewsStory extends Story
{
public function build(): void
{
ReviewFactory::createMany(200);
}
}
```
Edit your Fixtures:
```php
//src/DataFixtures/AppFixtures.php
namespace App\DataFixtures;
use App\Story\DefaultBooksStory;
use App\Story\DefaultReviewsStory;
use Doctrine\Bundle\FixturesBundle\Fixture;
use Doctrine\Persistence\ObjectManager;
class AppFixtures extends Fixture
{
public function load(ObjectManager $manager): void
{
DefaultBooksStory::load();
DefaultReviewsStory::load();
}
}
```
You can now load your fixtures in the database with the following command:
```console
docker compose exec php \
bin/console doctrine:fixtures:load
```
To learn more about fixtures, take a look at the documentation of [Foundry](https://symfony.com/bundles/ZenstruckFoundryBundle/current/index.html).
The list of available generators as well as a cookbook explaining how to create custom generators can be found in the documentation of [Faker](https://github.com/fakerphp/faker), the library used by Foundry under the hood.
## Writing Functional Tests
Now that you have some data fixtures for your API, you are ready to write functional tests with [PHPUnit](https://phpunit.de).
The API Platform test client implements the interfaces of the [Symfony HttpClient](https://symfony.com/doc/current/components/http_client.html). HttpClient is shipped with the API Platform distribution. The [Symfony test pack](https://github.com/symfony/test-pack/blob/main/composer.json), which includes PHPUnit as well as Symfony components useful for testing, is also included.
If you don't use the distribution, run `composer require --dev symfony/test-pack symfony/http-client` to install them.
Install [DAMADoctrineTestBundle](https://github.com/dmaicher/doctrine-test-bundle) to reset the database automatically before each test:
```console
docker compose exec php \
composer require --dev dama/doctrine-test-bundle
```
And activate it in the `phpunit.xml.dist` file:
```xml
```
Optionally, you can install [JSON Schema for PHP](https://github.com/justinrainbow/json-schema) if you want to use the [JSON Schema](https://json-schema.org) test assertions provided by API Platform:
```console
docker compose exec php \
composer require --dev justinrainbow/json-schema
```
Your API is now ready to be functionally tested. Create your test classes under the `tests/` directory.
Here is an example of functional tests specifying the behavior of [the bookstore API you created in the tutorial](index.md):
```php
request('GET', '/books');
$this->assertResponseIsSuccessful();
// Asserts that the returned content type is JSON-LD (the default)
$this->assertResponseHeaderSame('content-type', 'application/ld+json; charset=utf-8');
// Asserts that the returned JSON is a superset of this one
$this->assertJsonContains([
'@context' => '/contexts/Book',
'@id' => '/books',
'@type' => 'hydra:Collection',
'hydra:totalItems' => 100,
'hydra:view' => [
'@id' => '/books?page=1',
'@type' => 'hydra:PartialCollectionView',
'hydra:first' => '/books?page=1',
'hydra:last' => '/books?page=4',
'hydra:next' => '/books?page=2',
],
]);
// Because test fixtures are automatically loaded between each test, you can assert on them
$this->assertCount(30, $response->toArray()['hydra:member']);
// Asserts that the returned JSON is validated by the JSON Schema generated for this resource by API Platform
// This generated JSON Schema is also used in the OpenAPI spec!
$this->assertMatchesResourceCollectionJsonSchema(Book::class);
}
public function testCreateBook(): void
{
$response = static::createClient()->request('POST', '/books', ['json' => [
'isbn' => '0099740915',
'title' => 'The Handmaid\'s Tale',
'description' => 'Brilliantly conceived and executed, this powerful evocation of twenty-first century America gives full rein to Margaret Atwood\'s devastating irony, wit and astute perception.',
'author' => 'Margaret Atwood',
'publicationDate' => '1985-07-31T00:00:00+00:00',
]]);
$this->assertResponseStatusCodeSame(201);
$this->assertResponseHeaderSame('content-type', 'application/ld+json; charset=utf-8');
$this->assertJsonContains([
'@context' => '/contexts/Book',
'@type' => 'Book',
'isbn' => '0099740915',
'title' => 'The Handmaid\'s Tale',
'description' => 'Brilliantly conceived and executed, this powerful evocation of twenty-first century America gives full rein to Margaret Atwood\'s devastating irony, wit and astute perception.',
'author' => 'Margaret Atwood',
'publicationDate' => '1985-07-31T00:00:00+00:00',
'reviews' => [],
]);
$this->assertMatchesRegularExpression('~^/books/\d+$~', $response->toArray()['@id']);
$this->assertMatchesResourceItemJsonSchema(Book::class);
}
public function testCreateInvalidBook(): void
{
static::createClient()->request('POST', '/books', ['json' => [
'isbn' => 'invalid',
]]);
$this->assertResponseStatusCodeSame(422);
$this->assertResponseHeaderSame('content-type', 'application/ld+json; charset=utf-8');
$this->assertJsonContains([
'@context' => '/contexts/ConstraintViolationList',
'@type' => 'ConstraintViolationList',
'hydra:title' => 'An error occurred',
'hydra: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.
publicationDate: This value should not be null.',
]);
}
public function testUpdateBook(): void
{
// Only create the book we need with a given ISBN
BookFactory::createOne(['isbn' => '9781344037075']);
$client = static::createClient();
// findIriBy allows to retrieve the IRI of an item by searching for some of its properties.
$iri = $this->findIriBy(Book::class, ['isbn' => '9781344037075']);
// Use the PATCH method here to do a partial update
$client->request('PATCH', $iri, [
'json' => [
'title' => 'updated title',
],
'headers' => [
'Content-Type' => 'application/merge-patch+json',
]
]);
$this->assertResponseIsSuccessful();
$this->assertJsonContains([
'@id' => $iri,
'isbn' => '9781344037075',
'title' => 'updated title',
]);
}
public function testDeleteBook(): void
{
// Only create the book we need with a given ISBN
BookFactory::createOne(['isbn' => '9781344037075']);
$client = static::createClient();
$iri = $this->findIriBy(Book::class, ['isbn' => '9781344037075']);
$client->request('DELETE', $iri);
$this->assertResponseStatusCodeSame(204);
$this->assertNull(
// Through the container, you can access all your services from the tests, including the ORM, the mailer, remote API clients...
static::getContainer()->get('doctrine')->getRepository(Book::class)->findOneBy(['isbn' => '9781344037075'])
);
}
}
```
As you can see, the example uses the [trait `ResetDatabase`](https://symfony.com/bundles/ZenstruckFoundryBundle/current/index.html#database-reset)
from [Foundry](https://github.com/zenstruck/foundry) which will, at the beginning of each
test, purge the database, begin a transaction, and, at the end of each test, roll back the
transaction previously begun. Because of this, you can run your tests without worrying about fixtures.
There is one caveat though: in some tests, it is necessary to perform multiple requests in one test, for example when creating a user via the API and checking that a subsequent login using the same password works. However, the client will by default reboot the kernel, which will reset the database. You can prevent this by adding `$client->disableReboot();` to such tests.
All you have to do now is to run your tests:
```console
docker compose exec php \
bin/phpunit
```
If everything is working properly, you should see `OK (5 tests, 17 assertions)`.
Your REST API is now properly tested!
Check out the [testing documentation](../core/testing.md) to discover the full range of assertions and other features provided by API Platform's test utilities.
## Writing Unit Tests
In addition to integration tests written using the helpers provided by `ApiTestCase`, all the classes of your project should be covered by [unit tests](https://en.wikipedia.org/wiki/Unit_testing).
To do so, learn how to write unit tests with [PHPUnit](https://phpunit.de/) and [its Symfony/API Platform integration](https://symfony.com/doc/current/testing.html).
## Continuous Integration, Continuous Delivery and Continuous Deployment
Running your test suite in your [CI/CD pipeline](https://en.wikipedia.org/wiki/Continuous_integration) is important to ensure good quality and delivery time.
The API Platform distribution is [shipped with a GitHub Actions workflow](https://github.com/api-platform/api-platform/blob/main/.github/workflows/ci.yml) that builds the Docker images, does a [smoke test](https://en.wikipedia.org/wiki/Smoke_testing_(software)) to check that the application's entrypoint is accessible, and runs PHPUnit.
The API Platform Demo [contains a CD workflow](https://github.com/api-platform/demo/tree/main/.github/workflows) that uses [the Helm chart provided with the distribution](../deployment/kubernetes.md) to deploy the app on a Kubernetes cluster.
## Additional and Alternative Testing Tools
You may also be interested in these alternative testing tools (not included in the API Platform distribution):
* [Hoppscotch](https://docs.hoppscotch.io/features/tests), create functional test for your API
* [Hoppscotch](https://docs.hoppscotch.io/documentation/features/rest-api-testing/), create functional test for your API
Platform project using a nice UI, benefit from its Swagger integration and run tests in the CI using [the command-line tool](https://docs.hoppscotch.io/cli);
* [Behat](https://behat.org), a
[behavior-driven development (BDD)](https://en.wikipedia.org/wiki/Behavior-driven_development) framework to write the API
specification as user stories and in natural language then execute these scenarios against the application to validate
its behavior;
* [Blackfire Player](https://blackfire.io/player), a nice DSL to crawl HTTP services, assert responses, and extract data
from HTML/XML/JSON responses;
* [PHP Matcher](https://github.com/coduo/php-matcher), the Swiss Army knife of JSON document testing.
## Using the API Platform Distribution for End-to-End Testing
If you would like to verify that your stack (including services such as the DBMS, web server, [Varnish](https://varnish-cache.org/))
works, you need [end-to-end (E2E) testing](https://wiki.c2.com/?EndToEndPrinciple). To do so, we recommend using [Playwright](https://playwright.dev) if you use have PWA/JavaScript-heavy app, or [Symfony Panther](https://github.com/symfony/panther) if you mostly use Twig.
Usually, E2E testing should be done with a production-like setup. For your convenience, you may [run our Docker Compose setup
for production locally](../deployment/docker-compose.md#running-the-docker-compose-setup-for-production-locally).