Api Tests & Assertions

Keep on Learning!

If you liked what you've learned so far, dive in!
Subscribe to get access to this tutorial plus
video, code and script downloads.

Start your All-Access Pass
Buy just this tutorial for $12.00

With a Subscription, click any sentence in the script to jump to that part of the video!

Login Subscribe

Time to test our API! When someone uses our API for real, they'll use some sort of HTTP client - whether it be in JavaScript, PHP, Python, whatever. So, no surprise that to test our API, we'll do the exact same thing. Create a client object with $client = self::createClient().

... lines 1 - 6
class CheeseListingResourceTest extends ApiTestCase
{
public function testCreateCheeseListing()
{
$client = self::createClient();
... lines 12 - 14
}
}

This creates a, sort of, "fake" client, which is another feature that comes from the API Platform test classes. I say "fake" client because instead of making real HTTP requests to our domain, it makes them directly into our Symfony app via PHP... which just makes life a bit easier. And, side note, this $client object has the same interface as Symfony's new http-client component. So if you like how this works, next time you need to make real HTTP requests in PHP, try installing symfony/http-client instead of Guzzle.

Making Requests

Let's do this! Make a request with $client->request(): make a POST request to /api/cheeses.

How nice is that? We're going to focus our tests mostly on asserting security stuff. Because we haven't logged in, this request will not be authenticated... and so our access control rules should block access. Since we're anonymous, that should result in a 401 status code. Let's assert that! $this->assertResponseStatusCodeSame(401).

... lines 1 - 8
public function testCreateCheeseListing()
{
... lines 11 - 12
$client->request('POST', '/api/cheeses');
$this->assertResponseStatusCodeSame(401);
}
... lines 16 - 17

That assertion is not part of PHPUnit: we get that - and a bunch of other nice assertions - from API Platform's test classes.

Let's try this! Run the test:

php bin/phpunit

Deprecation Warnings?

Oh, interesting. At the bottom, we see deprecation warnings! This is a feature of the PHPUnit bridge: if our tests cause deprecated code to be executed, it prints those details after running the tests. These deprecations are coming from API Platform itself. They're already fixed in the next version of API Platform... so it's nothing we need to worry about. The warnings are a bit annoying... but we'll ignore them.

Missing symfony/http-client

Above all this stuff... oh... interesting. It died with

Call to undefined method: Client::prepareRequest()

What's going on here? Well... we're missing a dependency. Run

composer require symfony/http-client

API Platform's testing tools depend on this library. That "undefined" method is a pretty terrible error...it wasn't obvious at all how we should fix this. But there's already an issue on API Platform's issue tracker to throw a more clear error in this situation. It should say:

Hey! If you want to use the testing tools, please run composer require symfony/http-client

That's what we did! I also could have added the --dev flag... since we only need this for our tests... but because I might need to use the http-client component later inside my actual app, I chose to leave it off.

Ok, let's try those tests again:

php bin/phpunit

Content-Type Header

Oooh, it failed! The response contains an error! Oh...cool - we automatically get a nice view of that failed response. We're getting back a

406 Not acceptable

In the body... reading the error in JSON... is not so easy... but... let's see, here it is:

The content-type application/x-www-form-urlencoded is not supported.

We talked about this earlier! When we used the Axios library in JavaScript, I mentioned that when you POST data, there are two "main" ways to format the data in the request. The first way, and the way that most HTTP clients use by default, is to send in a format called application/x-www-form-urlencoded. Your browser sends data in this format when you submit a form. The second format - and the one that Axios uses by default - is to send the data as JSON.

Right now... well... we're not actually sending any data with this request. But if we did send some data, by default, this client object would format that data as application/x-www-form-urlencoded. And... looking at our API docs, our API expects data as JSON.

So even though we're not sending any data yet, the client is already sending a Content-Type header set to application/x-www-form-urlencoded. API Platform reads this and says:

Woh, woh woh! You're trying to send me data in the wrong format! 406 status code to you!

The most straightforward way to fix this is to change that header. Add a third argument - an options array - with a headers option to another array, and Content-Type set to application/json.

... lines 1 - 8
public function testCreateCheeseListing()
{
... line 11
$client->request('POST', '/api/cheeses', [
'headers' => ['Content-Type' => 'application/json']
]);
... line 15
}
... lines 17 - 18

Ok, try the tests again:

php bin/phpunit

This time... 400 Bad Request. Progress! Down below... we see there was a syntax error coming from some JsonDecode class. Of course! We're saying that we're sending JSON data... but we're actually sending no data. Any empty string is technically invalid JSON.

Add another key to the options array: json set to an empty array.

... lines 1 - 8
public function testCreateCheeseListing()
{
... line 11
$client->request('POST', '/api/cheeses', [
... line 13
'json' => [],
]);
... line 16
}
... lines 18 - 19

This is a really nice option: we pass it an array, and then the client will automatically json_encode that for us and send that as the body of the request. It gives us behavior similar to Axios. We're not sending any data yet... because we shouldn't have to: we should be denied access before validation is executed.

Let's try that next! We'll also talk about a security "gotcha" then finish this test by creating a user and logging in.

Leave a comment!

  • 2020-03-02 Victor Bocharsky

    Hey Skylar,

    Well done! Yes, that class came from "symfony/http-client" package.

    Cheers!

  • 2020-02-29 Skylar Scotlynn Gutman

    I fixed it by running composer require --dev symfony/browser-kit symfony/http-client

  • 2020-02-29 Skylar Scotlynn Gutman

    Hello,

    I get a different error when I run phpunit:
    PHP Fatal error: During class fetch: Uncaught ReflectionException: Class Symfony\Component\HttpClient\HttpClientTrait not found

  • 2020-01-03 Diego Aguiar

    Yep, ApiPlatform has changed a some things in its latest version. Thanks for sharing it!

  • 2020-01-02 Gabb

    In my case v2.5.3 all I had to do was:




    public function testCreatePost()
    {
    $client = self::createClient();


    $client->request('POST', '/api/posts');
    self::assertResponseStatusCodeSame(401);
    }


    Seems it was updated by api platform, authentication should be the first check before anything.

  • 2019-10-07 weaverryan

    Hey Александр Шебанин !

    Ok, I just checked our code using API Platform 2.5 to be sure. And, it *does* work... meaning, it *should* work for you... and I'm not sure why it doesn't. However, with a little bit of debugging, we can figure out what's going on :).

    Unless I'm mistaken, the problem is that, for some reason, your test.client service (which should be an instance of KernelBrowser - https://github.com/symfony/... ) is an instance of Client. You can prove this by running:


    php bin/console debug:container test.client --env=test

    On my system, I see that the class is KernelBrowser, I expect you will see Client. If so, is it possible you (or some bundle you're using) has overrode that service?

    Let me know!

    Cheers!

  • 2019-10-06 Александр Шебанин

    Hello. Due to implementing test case i faced some problem:

    ❯ php bin/phpunit
    PHPUnit 8.2.5 by Sebastian Bergmann and contributors.

    Testing Project Test Suite
    E 1 / 1 (100%)

    Time: 5.59 seconds, Memory: 50.25 MB

    There was 1 error:

    1) App\Tests\Functional\FooResourceTest::testCreateFoo
    TypeError: Argument 1 passed to ApiPlatform\Core\Bridge\Symfony\Bundle\Test\Client::__construct() must be an instance of Symfony\Bundle\FrameworkBundle\KernelBrowser, instance of Symfony\Bundle\FrameworkBundle\Client given, called in /Projects/ProjectName/var/cache/test/ContainerGLLQM1K/getTest_ApiPlatform_ClientService.php on line 19

    Actually i don't know what is wrong with my configs. I have Api platform 2.5 and symfony 4.3

    Regards,
    Alexander

  • 2019-09-02 Greg

    That's exactly that ;)

    Cheers

  • 2019-08-30 Diego Aguiar

    Hey Greg

    I believe it's due to your PHPUnit version. You can try upgrading PHPUnit, or remove the type-hints (string, bool) from App\ApiPlatform\Test\Constraint\ArraySubset::evaluate so it matches the signature of PHPUnit\Framework\Constraint\Constraint::evaluate

    Cheers!

  • 2019-08-30 Diego Aguiar

    NP man!

    Yeah, probably they didn't realize the impact of the change or they didn't have any other alternative. Who knows...

  • 2019-08-30 Greg

    Hi,

    I don't understand why I have this error


    Symfony\Component\Config\Exception\LoaderLoadException: Declaration of App\ApiPlatform\Test\Constraint\ArraySubset::evaluate($other, string $description = '', bool $returnResult = false) should be compatible with PHPUnit\Framework\Constraint\Constraint::evaluate($other, $description = '', $returnResult = false) in /apiPlatformPart2/config/services.yaml (which is loaded in resource "/apiPlatformPart2/config/services.yaml").

    I use phpunit 7.5 version

    Regards,
    Greg

  • 2019-08-29 Ramazan

    Hey Diego Aguiar ,

    Thank you for your deep dive into the problem.
    Interesting that http-client had made a so big change between 4.3.3 and 4.3.4, I thought that the 3rd one was mainly for bug/security fixes.

  • 2019-08-29 Diego Aguiar

    Hey Ramazan

    I found the reason of this problem. In the latest version of symfony/http-client (4.3.4) it's a change in the way that headers are parsed, you can see it here (https://github.com/symfony/... ). So, the code that Ryan copied from ApiPlatform future version needs to change as well.
    We have 2 options:
    A) Use symfony/http-client 4.3.3 version or
    B) Adjust the code as you mentioned above

    Cheers!

  • 2019-08-27 Ramazan

    Hi Diego Aguiar,

    I gave the right headers but when I run the test it didn't get it, so I got this error:


    2019-08-27T14:05:44+00:00 [error] Uncaught PHP Exception Symfony\Component\HttpKernel\Exception\NotAcceptableHttpException: "The content-type "application/x-www-form-urlencoded" is not supported. Supported MIME types are "application/ld+json", "application/json", "text/csv", "application/hal+json"." at /application/vendor/api-platform/core/src/EventListener/DeserializeListener.php line 128

    The $options['headers'] became like this:

    array(2) {
    [0]=>
    string(30) "Content-Type: application/json"
    [1]=>
    string(27) "accept: application/ld+json"
    }

    So the final array looks like this before I did the changes:

    array(2) {
    ["HTTP_0"]=>
    string(1) "C"
    ["HTTP_1"]=>
    string(1) "a"
    }

    FYI I don't use your source code in my project.
    Here is my actual symfony.lock file:
    https://gist.github.com/rakodev/2b80a2efbdddea8895e1bcb1a7f524e1

  • 2019-08-27 Diego Aguiar

    Hey Ramazan

    Sorry but I don't fully get the reason of that change. Could you tell me what error are you getting when running the test just as shown in the video?

    Cheers!

  • 2019-08-27 Ramazan

    Hello,

    FYI, to be able to run the test properly I had to modify your Client::request method like this:

    ...

    // line 95
    foreach ($options['headers'] as $key => $value) {
    [$key, $value] = explode(':', $value, 2);
    $value = trim($value);
    if ('Content-Type' === $key) {
    $server['CONTENT_TYPE'] = $value ?? '';

    continue;
    }
    $server['HTTP_'.strtoupper(str_replace('-', '_', $key))] = $value ?? '';
    }

    ...