Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add an HTTP client dedicated to functional API testing #2608

Merged
merged 4 commits into from
Jun 26, 2019

Conversation

dunglas
Copy link
Member

@dunglas dunglas commented Mar 12, 2019

Q A
Bug fix? no
New feature? yes
BC breaks? no
Deprecations? no
Tests pass? yes
Fixed tickets n/a
License MIT
Doc PR api-platform/docs#875

Currently, there are no very satisfactory solutions to write functional tests for web APIs built in PHP (API Platform and Symfony included):

  • Mink/Behat/Behatch are BDD-oriented (which is completely fine, but doesn't fit with all projects/teams), are a bit complex to setup, have a high barrier to entry and are still not fully compatible with Symfony 4 (you need to rely on a dev version of Behat). They also lack of utilities, for database testing for instance.
  • WebTestCase/BrowserKit are dedicated to webpage testing through web scraping (DOM Crawler, CSS selectors, simulation of browsers actions such as clicking or reloading a page...). Their API don't fit well with API testing. However, they benefit form the large ecosystem of PHPUnit, and give access to the numerous functional testing helpers provided by Symfony.
  • External solutions that don't manipulate the Symfony Kernel (but are true HTTP clients) such as Postman or BlackFire Player require to setup a web server for the testing env and don't give access to the service container (for instance, to test if the database has been updated, or if a mail has been sent).

It's time to say hi to ApiTestCase and Test\Client! This new set of API testing utilities is built on top of the Symfony's Client test class, and implements the exact same interface than the brand new Symfony HttpClient component.

As you'll see, it also plays very well with Alice (the fixture generator with an official Symfony recipe), that gets new power in the operation!

Let's see how it looks:

namespace App\Tests;

use ApiPlatform\Core\Bridge\Symfony\Bundle\Test\ApiTestCase;
use Dunglas\UserBundle\Entity\ForgotPassword\Jti;
use Hautelook\AliceBundle\PhpUnit\RefreshDatabaseTrait;
use Lcobucci\JWT\Parser;

class UsersTest extends ApiTestCase
{
    use RefreshDatabaseTrait; // This new trait I added to HautelookAliceBundle will take care of refreshing the database content to put it in a known state between every tests

     // A simple test
    public function testCreateUser()
    {
        $client = static::createClient(); // $client implements the new Symfony's HttpClientInterface
        $response = $client->request('POST', '/api/users', ['json' => [
            'email' => 'dunglas+test@gmail.com',
            'plainPassword' => 'foo',
        ]]);
        $this->assertSame(201, $response->getStatusCode());

        $responseContent = $response->toArray();
        $this->assertStringStartsWith('/api/users/', $responseContent['@id']);

        $this->assertArraySubset([
            '@type' => 'http://schema.org/Person',
            'email' => 'dunglas+test@gmail.com',
        ], $responseContent);
        $this->assertArrayNotHasKey('password', $responseContent);
        $this->assertArrayNotHasKey('plainPassword', $responseContent);
    }

    // something more advanced, accessing the mails and the DB
    public function testChangePassword()
    {
        $client = static::createClient();
        $client->enableProfiler();
        $client->disableReboot();

        // Send the request
        $response = $client->request('POST', '/api/users/token_request', ['json' => ['username' => 'dunglas@gmail.com']]);
        $this->assertSame(202, $response->getStatusCode());

        $mailCollector = $client->getProfile()->getCollector('swiftmailer');

        // extract the JWT contained in the mail
        preg_match('#[A-Za-z0-9-_=]+\.[A-Za-z0-9-_=]+\.?[A-Za-z0-9-_.+/=]*#', $mailCollector->getMessages()[0]->getBody(), $matches);
        $jwt = $matches[0];
        $decodedToken = (new Parser())->parse($jwt);

        // Change the password using the provided token
        $response = $client->request('PUT', $decodedToken->getClaim('sub'), [
            'json' => ['plainPassword' => 'newPassword'],
            'auth_bearer' => $jwt,
        ]);
        $this->assertSame(200, $response->getStatusCode());

        // Checks that the user have been changed
        $user = self::$container->get('test.security.helper')->getUser();
        $encoder = self::$container->get('test.security.encoder_factory')->getEncoder($user);
        $this->assertTrue($encoder->isPasswordValid($user->getPassword(), 'newPassword', $user->getSalt()));

        // Checks that the JTI has been saved
        $this->assertNotNull(
            self::$container->get('test.doctrine')->getRepository(Jti::class)->find($decodedToken->getClaim('jti'))
        );
    }
}

The following fixtures are loaded automatically. The database is reseted to a known test between every test (regardless of it fails or not) using transactions for maximum performance.

App\Entity\User:
  user_kevin:
    email: dunglas@gmail.com
    password: \$argon2i\$v=19\$m=1024,t=2,p=2\$TW9YajBjdGpUVUp4Uks1aA\$vwFQakIF9aGGoC5KjT+S3kuOYDtwtghzMuEZ/xP6FnM # maman

TODO:

  • Import tests
  • Write docs
  • Add support for Basic auth
  • Wait for Symfony 4.3

composer.json Outdated Show resolved Hide resolved
@baudev
Copy link

baudev commented Mar 12, 2019

It could be great adding the possibility of testing objects with the Symfony Validator.
I recently created a gist (link) concerning that.

It would allow testing an object with the following assertion:

$this->assertSymfonyValidate($objectToTest);

What do you think about implementing it in ApiTestCase?

@dunglas
Copy link
Member Author

dunglas commented Mar 12, 2019

@baudev I plan to add extra assertions in other PRs (like a response against a JSON Schema), but the one you're talking about should be added directly to Symfony IMHO (it can benefit to non-API Platform projects as well).

@teohhanhui
Copy link
Contributor

Actually, is there any reason why this goes into API Platform instead of in Symfony? I can see that the implementation is completely decoupled from API Platform?

@dunglas
Copy link
Member Author

dunglas commented Mar 13, 2019

@teohhanhui there are some API Platform specifics (JSON-LD by default), and @bendavies and I plan to add more (specific helpers to navigate in links, JSON Schema assertions...). But the current implementation could go in Symfony FrameworkBundle yes, technically speaking. That being said, I prefer to keep API-related stuff in this project, and this client is definitely API-related.

@dunglas
Copy link
Member Author

dunglas commented Mar 13, 2019

I just added a nice API to deal with API errors (validation errors for instance), and Basic authentication:

namespace App\Tests;

use ApiPlatform\Core\Bridge\Symfony\Bundle\Test\ApiTestCase;
use Hautelook\AliceBundle\PhpUnit\RefreshDatabaseTrait;
use Symfony\Contracts\HttpClient\Exception\ClientExceptionInterface;

class ProjectsTest extends ApiTestCase
{
    use RefreshDatabaseTrait;

    public function testNotAbleToCreateProjectWithoutOwner()
    {
        $this->expectException(ClientExceptionInterface::class);
        $this->expectExceptionCode(400); // HTTP status code
        $this->expectExceptionMessage(<<<ERROR
users: This collection should contain 1 element or more.
users: The current logged in user must be part of the users owning this resource.
ERROR
);

        $client = static::createClient();
        $response = $client->request('POST', '/api/projects', [
            'basic_auth' => ['dunglas@gmail.com', 'maman'],
            'json' => [
                'name' => 'My project',
            ],
        ]);
        $response->getContent();
    }
}

To get these nice error messages pre-populated, and to be able to assert on them your API just have to respect the Hydra convention for errors, or the RFC 7807 one).

@bendavies
Copy link
Contributor

bendavies commented Mar 13, 2019

i would prefer optional throwing (configurable?). in my tests, is manually check response codes and response content on error.

@dunglas
Copy link
Member Author

dunglas commented Mar 13, 2019

@bendavies you can do it too, it's natively supported by HttpClient, just pass false to $response->getContent() and it will not throw, then you'll be able to do your custom assertions.

@bendavies
Copy link
Contributor

bendavies commented Mar 13, 2019

yeah thanks i should have remembered that... (you mean false)

symfony-splitter pushed a commit to symfony/http-client that referenced this pull request Mar 17, 2019
This PR was squashed before being merged into the 4.3-dev branch (closes #30549).

Discussion
----------

[HttpClient] Make exceptions public

| Q             | A
| ------------- | ---
| Branch?       | master
| Bug fix?      | no
| New feature?  | no<!-- don't forget to update src/**/CHANGELOG.md files -->
| BC breaks?    | no     <!-- see https://symfony.com/bc -->
| Deprecations? | no <!-- don't forget to update UPGRADE-*.md and src/**/CHANGELOG.md files -->
| Tests pass?   | yes    <!-- please add some, will be required by reviewers -->
| Fixed tickets | #...   <!-- #-prefixed issue number(s), if any -->
| License       | MIT
| Doc PR        | n/a

Makes it easier to implement the interface. See api-platform/core#2608

Commits
-------

928d774e4a [HttpClient] Make exceptions public
fabpot added a commit to symfony/symfony that referenced this pull request Mar 17, 2019
This PR was squashed before being merged into the 4.3-dev branch (closes #30549).

Discussion
----------

[HttpClient] Make exceptions public

| Q             | A
| ------------- | ---
| Branch?       | master
| Bug fix?      | no
| New feature?  | no<!-- don't forget to update src/**/CHANGELOG.md files -->
| BC breaks?    | no     <!-- see https://symfony.com/bc -->
| Deprecations? | no <!-- don't forget to update UPGRADE-*.md and src/**/CHANGELOG.md files -->
| Tests pass?   | yes    <!-- please add some, will be required by reviewers -->
| Fixed tickets | #...   <!-- #-prefixed issue number(s), if any -->
| License       | MIT
| Doc PR        | n/a

Makes it easier to implement the interface. See api-platform/core#2608

Commits
-------

928d774 [HttpClient] Make exceptions public
symfony-splitter pushed a commit to symfony/http-client that referenced this pull request Mar 24, 2019
… exception messages (dunglas)

This PR was squashed before being merged into the 4.3-dev branch (closes #30559).

Discussion
----------

[HttpClient] Parse common API error formats for better exception messages

| Q             | A
| ------------- | ---
| Branch?       | master <!-- see below -->
| Bug fix?      | no
| New feature?  | yes <!-- don't forget to update src/**/CHANGELOG.md files -->
| BC breaks?    | no     <!-- see https://symfony.com/bc -->
| Deprecations? | no <!-- don't forget to update UPGRADE-*.md and src/**/CHANGELOG.md files -->
| Tests pass?   | yes    <!-- please add some, will be required by reviewers -->
| Fixed tickets | n/a  <!-- #-prefixed issue number(s), if any -->
| License       | MIT
| Doc PR        | todo?

Use extra details provided by popular error formats following to improve HTTP exception messages.
The following formats are supported:

* Hydra (default in API Platform)
* RFC 7807 (followed by Symfony's [ConstraintViolationListNormalizer](https://github.com/symfony/symfony/blob/master/src/Symfony/Component/Serializer/Normalizer/ConstraintViolationListNormalizer.php) and supported by API Platform and Apigility)
* JSON:API (because it respects the semantic of the RFC 7807)

It allows to write code like the following (here in a test context):

```php
    public function testBadRequest()
    {
        $this->expectException(ClientExceptionInterface::class);
        $this->expectExceptionCode(400); // HTTP status code
        $this->expectExceptionMessage(<<<ERROR
Validation Failed

users: This collection should contain 1 element or more.
users: The current logged in user must be part of the users owning this resource.
ERROR
);

        $response = (HttpClient::create())->request('POST', 'http://example.com/api/projects', [
            'json' => [
                'name' => 'My project',
            ],
        ]);
        $response->getContent();
    }
```

Port of api-platform/core#2608 (comment).

Commits
-------

96df4464a1 [HttpClient] Parse common API error formats for better exception messages
fabpot added a commit to symfony/symfony that referenced this pull request Mar 24, 2019
… exception messages (dunglas)

This PR was squashed before being merged into the 4.3-dev branch (closes #30559).

Discussion
----------

[HttpClient] Parse common API error formats for better exception messages

| Q             | A
| ------------- | ---
| Branch?       | master <!-- see below -->
| Bug fix?      | no
| New feature?  | yes <!-- don't forget to update src/**/CHANGELOG.md files -->
| BC breaks?    | no     <!-- see https://symfony.com/bc -->
| Deprecations? | no <!-- don't forget to update UPGRADE-*.md and src/**/CHANGELOG.md files -->
| Tests pass?   | yes    <!-- please add some, will be required by reviewers -->
| Fixed tickets | n/a  <!-- #-prefixed issue number(s), if any -->
| License       | MIT
| Doc PR        | todo?

Use extra details provided by popular error formats following to improve HTTP exception messages.
The following formats are supported:

* Hydra (default in API Platform)
* RFC 7807 (followed by Symfony's [ConstraintViolationListNormalizer](https://github.com/symfony/symfony/blob/master/src/Symfony/Component/Serializer/Normalizer/ConstraintViolationListNormalizer.php) and supported by API Platform and Apigility)
* JSON:API (because it respects the semantic of the RFC 7807)

It allows to write code like the following (here in a test context):

```php
    public function testBadRequest()
    {
        $this->expectException(ClientExceptionInterface::class);
        $this->expectExceptionCode(400); // HTTP status code
        $this->expectExceptionMessage(<<<ERROR
Validation Failed

users: This collection should contain 1 element or more.
users: The current logged in user must be part of the users owning this resource.
ERROR
);

        $response = (HttpClient::create())->request('POST', 'http://example.com/api/projects', [
            'json' => [
                'name' => 'My project',
            ],
        ]);
        $response->getContent();
    }
```

Port of api-platform/core#2608 (comment).

Commits
-------

96df446 [HttpClient] Parse common API error formats for better exception messages
@dunglas dunglas force-pushed the test-client branch 2 times, most recently from 478116b to 7d7d038 Compare June 24, 2019 20:58
Copy link
Member

@soyuka soyuka left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this stuff rocks

src/Bridge/Symfony/Bundle/Test/Response.php Show resolved Hide resolved
tests/Bridge/Symfony/Bundle/Test/ClientTest.php Outdated Show resolved Hide resolved
@dunglas dunglas merged commit 44c5080 into api-platform:master Jun 26, 2019
@dunglas dunglas deleted the test-client branch June 26, 2019 17:00
fabpot added a commit to symfony/symfony that referenced this pull request Jul 4, 2019
…ns with Panther and API Platform's test client (dunglas)

This PR was submitted for the 4.3 branch but it was merged into the 4.4 branch instead (closes #32207).

Discussion
----------

[FrameworkBundle] Allow to use the BrowserKit assertions with Panther and API Platform's test client

| Q             | A
| ------------- | ---
| Branch?       | 4.3
| Bug fix?      | yes
| New feature?  | no <!-- please update src/**/CHANGELOG.md files -->
| BC breaks?    | no     <!-- see https://symfony.com/bc -->
| Deprecations? | no <!-- please update UPGRADE-*.md and src/**/CHANGELOG.md files -->
| Tests pass?   | yes    <!-- please add some, will be required by reviewers -->
| Fixed tickets | n/a
| License       | MIT
| Doc PR        | n/a

I'm adding a testing client for API Platform that implements the interfaces of HttpClient: api-platform/core#2608
Most PHPUnit assertions provided by Symfony are useful and can be reused, but the ones using the crawler are not relevant and pollute auto-complete suggestions (because a web API usually returns JSON, not HTML).

This PR splits the existing trait to allow reusing the HTTP related assertions only.

Commits
-------

cd0341e [FrameworkBundle] Allow to use the BrowserKit assertions with Panther and API Platform's test client
sadafrangian3 pushed a commit to sadafrangian3/Dependency-Injection-http-client that referenced this pull request Nov 2, 2022
This PR was squashed before being merged into the 4.3-dev branch (closes #30549).

Discussion
----------

[HttpClient] Make exceptions public

| Q             | A
| ------------- | ---
| Branch?       | master
| Bug fix?      | no
| New feature?  | no<!-- don't forget to update src/**/CHANGELOG.md files -->
| BC breaks?    | no     <!-- see https://symfony.com/bc -->
| Deprecations? | no <!-- don't forget to update UPGRADE-*.md and src/**/CHANGELOG.md files -->
| Tests pass?   | yes    <!-- please add some, will be required by reviewers -->
| Fixed tickets | #...   <!-- #-prefixed issue number(s), if any -->
| License       | MIT
| Doc PR        | n/a

Makes it easier to implement the interface. See api-platform/core#2608

Commits
-------

928d774e4a [HttpClient] Make exceptions public
sadafrangian3 pushed a commit to sadafrangian3/Dependency-Injection-http-client that referenced this pull request Nov 2, 2022
… exception messages (dunglas)

This PR was squashed before being merged into the 4.3-dev branch (closes #30559).

Discussion
----------

[HttpClient] Parse common API error formats for better exception messages

| Q             | A
| ------------- | ---
| Branch?       | master <!-- see below -->
| Bug fix?      | no
| New feature?  | yes <!-- don't forget to update src/**/CHANGELOG.md files -->
| BC breaks?    | no     <!-- see https://symfony.com/bc -->
| Deprecations? | no <!-- don't forget to update UPGRADE-*.md and src/**/CHANGELOG.md files -->
| Tests pass?   | yes    <!-- please add some, will be required by reviewers -->
| Fixed tickets | n/a  <!-- #-prefixed issue number(s), if any -->
| License       | MIT
| Doc PR        | todo?

Use extra details provided by popular error formats following to improve HTTP exception messages.
The following formats are supported:

* Hydra (default in API Platform)
* RFC 7807 (followed by Symfony's [ConstraintViolationListNormalizer](https://github.com/symfony/symfony/blob/master/src/Symfony/Component/Serializer/Normalizer/ConstraintViolationListNormalizer.php) and supported by API Platform and Apigility)
* JSON:API (because it respects the semantic of the RFC 7807)

It allows to write code like the following (here in a test context):

```php
    public function testBadRequest()
    {
        $this->expectException(ClientExceptionInterface::class);
        $this->expectExceptionCode(400); // HTTP status code
        $this->expectExceptionMessage(<<<ERROR
Validation Failed

users: This collection should contain 1 element or more.
users: The current logged in user must be part of the users owning this resource.
ERROR
);

        $response = (HttpClient::create())->request('POST', 'http://example.com/api/projects', [
            'json' => [
                'name' => 'My project',
            ],
        ]);
        $response->getContent();
    }
```

Port of api-platform/core#2608 (comment).

Commits
-------

96df4464a1 [HttpClient] Parse common API error formats for better exception messages
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

9 participants