From 327c6d5f7a6bf2c8357013b5c26192cea89d50d9 Mon Sep 17 00:00:00 2001 From: Vincent Amstoutz Date: Tue, 1 Oct 2024 10:19:56 +0200 Subject: [PATCH] refactor: split test docs and add laravel support --- core/getting-started.md | 2 +- core/json-schema.md | 6 +- core/testing.md | 215 +---------------- laravel/testing.md | 517 ++++++++++++++++++++++++++++++++++++++++ outline.yaml | 1 + symfony/testing.md | 230 +++++++++++++++++- 6 files changed, 750 insertions(+), 221 deletions(-) create mode 100644 laravel/testing.md diff --git a/core/getting-started.md b/core/getting-started.md index 8d60d799197..7b9071fb497 100644 --- a/core/getting-started.md +++ b/core/getting-started.md @@ -13,7 +13,7 @@ If you are starting a new project, the easiest way to get API Platform up is to It comes with the API Platform core library integrated with [the Symfony framework](https://symfony.com), [the schema generator](../schema-generator/), [Doctrine ORM](https://www.doctrine-project.org), -[NelmioCorsBundle](https://github.com/nelmio/NelmioCorsBundle) and [test assertions dedicated to APIs](testing.md). +[NelmioCorsBundle](https://github.com/nelmio/NelmioCorsBundle) and [test assertions dedicated to APIs](../symfony/testing-utilities.md). [MongoDB](mongodb.md) and [Elasticsearch](elasticsearch.md) can also be easily enabled. diff --git a/core/json-schema.md b/core/json-schema.md index aa180980cfc..afdb3022143 100644 --- a/core/json-schema.md +++ b/core/json-schema.md @@ -23,12 +23,12 @@ bin/console help api:json-schema:generate In a unit testing context, API Platform does not use the same schema version as the schema used when generating the API documentation. The version used by the documentation is the OpenAPI Schema version and the version used by unit testing is the JSON Schema version. -When [Testing the API](testing.md), JSON Schemas are useful to generate and automate unit testing. API Platform provides specific unit testing functionalities like [`assertMatchesResourceCollectionJsonSchema()`](testing.md#writing-functional-tests) or [`assertMatchesResourceItemJsonSchema()`](testing.md#writing-functional-tests) methods. +When [Testing the API](../symfony/testing-utilities.md), JSON Schemas are useful to generate and automate unit testing. API Platform provides specific unit testing functionalities like [`assertMatchesResourceCollectionJsonSchema()`](../symfony/testing-utilities.md#writing-functional-tests) or [`assertMatchesResourceItemJsonSchema()`](../symfony/testing-utilities.md#writing-functional-tests) methods. These methods generate a JSON Schema then do unit testing based on the generated schema automatically. Usually, the fact that API Platform uses a different schema version for unit testing is not a problem, but sometimes you may need to use the [`ApiProperty`](openapi.md#using-the-openapi-and-swagger-contexts) attribute to specify a [calculated field](serialization.md#calculated-field) type by overriding the OpenAPI Schema for the calculated field to be correctly documented. -When you will use [`assertMatchesResourceCollectionJsonSchema()`](testing.md#writing-functional-tests) or [`assertMatchesResourceItemJsonSchema()`](testing.md#writing-functional-tests) functions the unit test will fail on this [calculated field](serialization.md#calculated-field) as the unit testing process doesn't use the `openapi_context` you specified +When you will use [`assertMatchesResourceCollectionJsonSchema()`](../symfony/testing-utilities.md#writing-functional-tests) or [`assertMatchesResourceItemJsonSchema()`](../symfony/testing-utilities.md#writing-functional-tests) functions the unit test will fail on this [calculated field](serialization.md#calculated-field) as the unit testing process doesn't use the `openapi_context` you specified because API Platform is using the JSON Schema version instead at this moment. So there is a way to override JSON Schema specification for a specific property in the JSON Schema used by the unit testing process. @@ -82,4 +82,4 @@ To generate JSON Schemas programmatically, use the `api_platform.json_schema.sch ## Testing API Platform provides a PHPUnit assertion to test if a response is valid according to a given Schema: `assertMatchesJsonSchema()`. -Refer to [the testing documentation](testing.md) for more details. +Refer to [the testing documentation](../symfony/testing-utilities.md) for more details. diff --git a/core/testing.md b/core/testing.md index c99e91fea0f..480ae28fa24 100644 --- a/core/testing.md +++ b/core/testing.md @@ -1,210 +1,13 @@ -# Testing Utilities +# Testing the API -API Platform provides a set of useful utilities dedicated to API testing. -For an overview of how to test an API Platform app, be sure to read [the testing cookbook first](../symfony/testing.md). +Once your API is up and running, it's crucial to write tests to ensure it is bug-free and to prevent future regressions. +A good practice is to follow a [Test-Driven Development (TDD)](https://martinfowler.com/bliki/TestDrivenDevelopment.html) +approach, where tests are written before the production code. -

Test and Assertions screencast
Watch the API Tests & Assertions screencast

+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). -## The Test HttpClient +## Testing Documentations -API Platform provides its own implementation of the [Symfony HttpClient](https://symfony.com/doc/current/components/http_client.html)'s interfaces, tailored to be used directly in [PHPUnit](https://phpunit.de/) test classes. - -While all the convenient features of Symfony HttpClient are available and usable directly, under the hood the API Platform implementation manipulates [the Symfony HttpKernel](https://symfony.com/doc/current/components/http_kernel.html) directly to simulate HTTP requests and responses. -This approach results in a huge performance boost compared to triggering real network requests. -It also allows access to the [Symfony HttpKernel](https://symfony.com/doc/current/components/http_kernel.html) and to all your services via the [Dependency Injection Container](https://symfony.com/doc/current/testing.html#accessing-the-container). -Reuse them to run, for instance, SQL queries or requests to external APIs directly from your tests. - -Install the `symfony/http-client` and `symfony/browser-kit` packages to enabled the API Platform test client: - -```console -composer require symfony/browser-kit symfony/http-client -``` - -To use the testing client, your test class must extend the `ApiTestCase` class: - -```php -request('GET', '/books'); - // your assertions here... - } -} -``` - -Refer to [the Symfony HttpClient documentation](https://symfony.com/doc/current/components/http_client.html) to discover all the features of the client (custom headers, JSON encoding and decoding, HTTP Basic and Bearer authentication and cookies support, among other things). - -Note that you can create your own test case class extending the ApiTestCase. For example to set up a Json Web Token authentication: - -```php -getToken(); - - return static::createClient([], ['headers' => ['authorization' => 'Bearer '.$token]]); - } - - /** - * Use other credentials if needed. - */ - protected function getToken($body = []): string - { - if ($this->token) { - return $this->token; - } - - $response = static::createClient()->request('POST', '/login', ['json' => $body ?: [ - 'username' => 'admin@example.com', - 'password' => '$3cr3t', - ]]); - - $this->assertResponseIsSuccessful(); - $data = $response->toArray(); - $this->token = $data['token']; - - return $data['token']; - } -} -``` - -Use it by extending the `AbstractTest` class. For example this class tests the `/users` resource accessibility where only the admin can retrieve the collection: - -```php -createClientWithCredentials()->request('GET', '/users'); - $this->assertResponseIsSuccessful(); - } - - public function testLoginAsUser() - { - $token = $this->getToken([ - 'username' => 'user@example.com', - 'password' => '$3cr3t', - ]); - - $response = $this->createClientWithCredentials($token)->request('GET', '/users'); - $this->assertJsonContains(['description' => 'Access Denied.']); - $this->assertResponseStatusCodeSame(403); - } -} -``` - -## API Test Assertions - -In addition to [the built-in ones](https://phpunit.readthedocs.io/en/latest/assertions.html), API Platform provides convenient PHPUnit assertions dedicated to API testing: - -```php -request(...); - - // Asserts that the returned JSON is equal to the passed one - $this->assertJsonEquals(/* a JSON document as an array or as a string */); - - // Asserts that the returned JSON is a superset of the passed one - $this->assertJsonContains(/* a JSON document as an array or as a string */); - - // justinrainbow/json-schema must be installed to use the following assertions - - // Asserts that the returned JSON matches the passed JSON Schema - $this->assertMatchesJsonSchema(/* a JSON Schema as an array or as a string */); - - // Asserts that the returned JSON is validated by the JSON Schema generated for this resource by API Platform - - // For collections - $this->assertMatchesResourceCollectionJsonSchema(YourApiResource::class); - // And for items - $this->assertMatchesResourceItemJsonSchema(YourApiResource::class); - } -} -``` - -There is also a method to find the IRI matching a given resource and some criteria: - -```php -findIriBy(Book::class, ['isbn' => '9780451524935']); - static::createClient()->request('GET', $iri); - $this->assertResponseIsSuccessful(); - } -} -``` - -## HTTP Test Assertions - -All test assertions provided by Symfony (assertions for status codes, headers, cookies, XML documents...) can be used out of the box with the API Platform test client: - -```php -request('GET', '/books'); - - $this->assertResponseIsSuccessful(); - $this->assertResponseHeaderSame('content-type', 'application/ld+json; charset=utf-8'); - } -} -``` - -[Check out the dedicated Symfony documentation entry](https://symfony.com/doc/current/testing/functional_tests_assertions.html). +- If you are using API Platform with Symfony, refer to the [Testing the API with Symfony](/symfony/testing.md) documentation. +- If you are using API Platform with Laravel, refer to the [Testing the API with Laravel](/laravel/testing.md) documentation. diff --git a/laravel/testing.md b/laravel/testing.md new file mode 100644 index 00000000000..b9039d53dea --- /dev/null +++ b/laravel/testing.md @@ -0,0 +1,517 @@ +# Testing the API with Laravel + +For an introduction to testing using API Platform, refer to the [Core Testing Documentation](../core/testing.md), or access the +[Symfony Testing Guide](../symfony/testing.md). + +Let's learn how to use tests with Laravel! + +In this article, you'll learn how to use: + +- **[Pest](https://pestphp.com/)**: A testing framework that enables you to write unit tests for your classes and create +API-oriented functional tests, thanks to its integrations with API Platform and [Laravel](https://laravel.com/docs/testing). +- **[PHPUnit](https://phpunit.de)**: A testing framework for writing unit tests for your classes and conducting API-oriented +functional tests, with support for API Platform and [Laravel](https://laravel.com/docs/testing). + +> [!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. + +## Tests with Pest + +> [!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](https://pestphp.com/docs/writing-tests#content-assertion-api) documentation. + +### Installing Pest + +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: + +```console +php artisan test +``` + +If for some reason, Pest is not installed refer to the [Pest Installation Guide](https://pestphp.com/docs/installation). + +In that case, you can run Pest using: + +```console +./vendor/bin/pest +``` + +### Writing Functional Tests with Pest + +#### Generate the Factory + +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](https://laravel.com/docs/artisan) command. +For example, to create a factory for a Book model, run: + +```console +php artisan make:factory BookFactory +``` + +For advanced customization and configuration, refer to the [Defining model Factories Laravel Guide](https://laravel.com/docs/eloquent-factories#defining-model-factories). + +Then, you can now use your factory in tests to quickly generate model instances. + +#### Writing Pest tests + +Here’s an example of tests, which use the Factory: + +```php +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](https://laravel.com/docs/database-testing#resetting-the-database-after-each-test) +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. + +#### Run Pest tests + +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](#api-test-assertions-with-laravel) to discover the full range of assertions +and other features provided by API Platform's test utilities. + +### Migrating from PHPUnit to Pest + +If you want to migrate from PHPUnit to Pest, refer to [Migrating from PHPUnit Guide](https://pestphp.com/docs/migrating-from-phpunit-guide) +and [Installation Guide](https://pestphp.com/docs/installation). + +## Tests with PHPUnit + +### Installing PHPUnit + +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: +```console +./vendor/bin/phpunit --version +``` + +If for some reason, PHPUnit is not installed refer to the [PHPUnit Installation Guide](https://docs.phpunit.de/en/11.4/installation.html#installing-phpunit-with-composer). + +### Writing Functional Tests with PHPUnit + +For instructions on generating the factory, please refer to the [Generate The Factory section](#generate-the-factory). + +#### Writing PHPUnit tests + +Here’s an example of a test class, which use the Factory: + +```php +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](https://laravel.com/docs/database-testing#resetting-the-database-after-each-test) +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. + +#### Run PHPUnit tests + +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](#api-test-assertions-with-laravel) 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 Pest and PHPUnit, 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 [Pest](https://pestphp.com), [PHPUnit](https://phpunit.de/) and +[Laravel Creating Tests Guide](https://laravel.com/docs/11.x/testing#creating-tests). + +## 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]() 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 +- [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; +- [Playwright](https://playwright.dev) is recommended if you use have PWA/JavaScript-heavy app. + +## Testing Utilities for Laravel + +### API Test Assertions with Laravel + +In addition to [the built-in ones](https://phpunit.readthedocs.io/en/main/assertions.html), API Platform provides +convenient PHPUnit assertions dedicated to API testing: + +```php +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 +getIriFromResource($book); + + $response = $this->get($iri); + + $response->assertStatus(200); + } +} +``` diff --git a/outline.yaml b/outline.yaml index 27bbac705d6..66588235f1d 100644 --- a/outline.yaml +++ b/outline.yaml @@ -12,6 +12,7 @@ chapters: path: laravel items: - index + - testing - filters - security - validation diff --git a/symfony/testing.md b/symfony/testing.md index 7df71d83683..4f9cb3b810b 100644 --- a/symfony/testing.md +++ b/symfony/testing.md @@ -1,11 +1,9 @@ -# Testing the API +# Testing the API with Symfony -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). +For an introduction to testing using API Platform, refer to the [Core Testing Documentation](../core/testing.md), or access the +[Laravel Testing Guide](../laravel/testing.md). -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! +Let's learn how to use tests with Symfony!

Tests and Assertions screencast
Watch the Tests & Assertions screencast

@@ -344,7 +342,8 @@ 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. +Check out the [API Test Assertions section](#api-test-assertions-with-symfony) to discover the full range of assertions +and other features provided by API Platform's test utilities. ## Writing Unit Tests @@ -366,10 +365,8 @@ You may also be interested in these alternative testing tools (not included in t - [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; +- [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. @@ -381,3 +378,214 @@ works, you need [end-to-end testing](https://wiki.c2.com/?EndToEndPrinciple). To Usually, end-to-end 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). + +## Testing Utilities for Symfony + +API Platform provides a set of useful utilities dedicated to API testing. +For an overview of how to test an API Platform app, be sure to read [the testing part first](#testing-the-api-with-symfony). + +

Test and Assertions screencast
Watch the API Tests & Assertions screencast

+ +### The Test HttpClient + +API Platform provides its own implementation of the [Symfony HttpClient](https://symfony.com/doc/current/components/http_client.html)'s interfaces, tailored to be used directly in [PHPUnit](https://phpunit.de/) test classes. + +While all the convenient features of Symfony HttpClient are available and usable directly, under the hood the API Platform implementation manipulates [the Symfony HttpKernel](https://symfony.com/doc/current/components/http_kernel.html) directly to simulate HTTP requests and responses. +This approach results in a huge performance boost compared to triggering real network requests. +It also allows access to the [Symfony HttpKernel](https://symfony.com/doc/current/components/http_kernel.html) and to all your services via the [Dependency Injection Container](https://symfony.com/doc/current/testing.html#accessing-the-container). +Reuse them to run, for instance, SQL queries or requests to external APIs directly from your tests. + +Install the `symfony/http-client` and `symfony/browser-kit` packages to enable the API Platform test client: + +```console +composer require symfony/browser-kit symfony/http-client +``` + +To use the testing client, your test class must extend the `ApiTestCase` class: + +```php +request('GET', '/books'); + // your assertions here... + } +} +``` + +Refer to [the Symfony HttpClient documentation](https://symfony.com/doc/current/components/http_client.html) to discover all the features of the client (custom headers, JSON encoding and decoding, HTTP Basic and Bearer authentication and cookies support, among other things). + +Note that you can create your own test case class extending the ApiTestCase. For example to set up a Json Web Token authentication: + +```php +getToken(); + + return static::createClient([], ['headers' => ['authorization' => 'Bearer '.$token]]); + } + + /** + * Use other credentials if needed. + */ + protected function getToken($body = []): string + { + if ($this->token) { + return $this->token; + } + + $response = static::createClient()->request('POST', '/login', ['json' => $body ?: [ + 'username' => 'admin@example.com', + 'password' => '$3cr3t', + ]]); + + $this->assertResponseIsSuccessful(); + $data = $response->toArray(); + $this->token = $data['token']; + + return $data['token']; + } +} +``` + +Use it by extending the `AbstractTest` class. For example this class tests the `/users` resource accessibility where only the admin can retrieve the collection: + +```php +createClientWithCredentials()->request('GET', '/users'); + $this->assertResponseIsSuccessful(); + } + + public function testLoginAsUser() + { + $token = $this->getToken([ + 'username' => 'user@example.com', + 'password' => '$3cr3t', + ]); + + $response = $this->createClientWithCredentials($token)->request('GET', '/users'); + $this->assertJsonContains(['description' => 'Access Denied.']); + $this->assertResponseStatusCodeSame(403); + } +} +``` + +### API Test Assertions with Symfony + +In addition to [the built-in ones](https://phpunit.readthedocs.io/en/11.4/assertions.html), API Platform provides convenient PHPUnit assertions dedicated to API testing: + +```php +request(...); + + // Asserts that the returned JSON is equal to the passed one + $this->assertJsonEquals(/* a JSON document as an array or as a string */); + + // Asserts that the returned JSON is a superset of the passed one + $this->assertJsonContains(/* a JSON document as an array or as a string */); + + // justinrainbow/json-schema must be installed to use the following assertions + + // Asserts that the returned JSON matches the passed JSON Schema + $this->assertMatchesJsonSchema(/* a JSON Schema as an array or as a string */); + + // Asserts that the returned JSON is validated by the JSON Schema generated for this resource by API Platform + + // For collections + $this->assertMatchesResourceCollectionJsonSchema(YourApiResource::class); + // And for items + $this->assertMatchesResourceItemJsonSchema(YourApiResource::class); + } +} +``` + +There is also a method to find the IRI matching a given resource and some criteria: + +```php +findIriBy(Book::class, ['isbn' => '9780451524935']); + static::createClient()->request('GET', $iri); + $this->assertResponseIsSuccessful(); + } +} +``` + +### HTTP Test Assertions + +All test assertions provided by Symfony (assertions for status codes, headers, cookies, XML documents...) can be used out of the box with the API Platform test client: + +```php +request('GET', '/books'); + + $this->assertResponseIsSuccessful(); + $this->assertResponseHeaderSame('content-type', 'application/ld+json; charset=utf-8'); + } +} +``` + +[Check out the dedicated Symfony documentation entry](https://symfony.com/doc/current/testing/functional_tests_assertions.html).