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

[Tests Feature] Assert Exact JSON Structure #19871

Closed
brunocascio opened this issue Jul 3, 2017 · 9 comments
Closed

[Tests Feature] Assert Exact JSON Structure #19871

brunocascio opened this issue Jul 3, 2017 · 9 comments

Comments

@brunocascio
Copy link

brunocascio commented Jul 3, 2017

  • Laravel Version: 5.4.28
  • PHP Version: 7.1.5
  • Database Driver & Version: SQLite 3 (:memory:)

Description

Currently, I'm using assertJsonStructure in order to validate the structure, but it doesn't check the exact JSON structure, for example:

[
  {"id":1, "name": "Bueno Aires", "slug": "buenos-aires"},
  {"id":2, "name": "Neuquén", "slug": "neuquen"},
  {"id":3, "name": "Córdoba", "slug": "cordoba"}
]

Test results are:

assertJsonStructure([['id', 'name', 'slug']]);: true
assertJsonStructure([['id', 'name']]);: true

Test results expected:

assertJsonStructure([['id', 'name', 'slug']]);: true
assertJsonStructure([['id', 'name']]);: false

Is there some way to validate that?

@themsaid
Copy link
Member

themsaid commented Jul 3, 2017

Sorry please ask on the forums, this repo is for bug reporting only.

@themsaid themsaid closed this as completed Jul 3, 2017
@brunocascio
Copy link
Author

thanks @themsaid

For future related issues, https://laravel.io/forum/assert-exact-json-structure

@dmitrybubyakin
Copy link

dmitrybubyakin commented Apr 18, 2018

Here is my solution.

<?php

namespace Tests\Support;

use Illuminate\Foundation\Testing\TestResponse;
use PHPUnit\Framework\Assert as PHPUnit;

trait AssertJson
{
    public function setUpAssertJson()
    {
        TestResponse::macro('assertJsonStructureExact', function (array $structure = null, $responseData = null)
        {
            if (is_null($structure)) {
                return $this->assertJson($this->json());
            }

            if (is_null($responseData)) {
                $responseData = $this->decodeResponseJson();
            }

            if (! array_key_exists('*', $structure)) {
                $keys = array_map(function ($value, $key) {
                    return is_array($value) ? $key : $value;
                }, $structure, array_keys($structure));

                PHPUnit::assertEquals($keys, array_keys($responseData));
            }

            foreach ($structure as $key => $value) {
                if (is_array($value) && $key === '*') {
                    PHPUnit::assertInternalType('array', $responseData);

                    foreach ($responseData as $responseDataItem) {
                        $this->assertJsonStructureExact($structure['*'], $responseDataItem);
                    }
                } elseif (is_array($value)) {
                    PHPUnit::assertArrayHasKey($key, $responseData);

                    $this->assertJsonStructureExact($structure[$key], $responseData[$key]);
                } else {
                    PHPUnit::assertArrayHasKey($value, $responseData);
                }
            }

            return $this;
        });
    }
}

Usage:

class MyTestCase extends TestCase
{
    use AssertJson;

    public function setUp(): void
    {
        parent::setUp();

        $this->setUpAssertJson();
    }

    public function text_example()
    {
        $this->get('api/users')->assertJsonStructureExact(['data' => ['*' => ['id', 'email']]]);
    }
}

@MrJmpl3
Copy link

MrJmpl3 commented Mar 18, 2020

Here is my solution.

<?php

namespace Tests\Support;

use Illuminate\Foundation\Testing\TestResponse;
use PHPUnit\Framework\Assert as PHPUnit;

trait AssertJson
{
    public function setUpAssertJson()
    {
        TestResponse::macro('assertJsonStructureExact', function (array $structure = null, $responseData = null)
        {
            if (is_null($structure)) {
                return $this->assertJson($this->json());
            }

            if (is_null($responseData)) {
                $responseData = $this->decodeResponseJson();
            }

            if (! array_key_exists('*', $structure)) {
                $keys = array_map(function ($value, $key) {
                    return is_array($value) ? $key : $value;
                }, $structure, array_keys($structure));

                PHPUnit::assertEquals($keys, array_keys($responseData));
            }

            foreach ($structure as $key => $value) {
                if (is_array($value) && $key === '*') {
                    PHPUnit::assertInternalType('array', $responseData);

                    foreach ($responseData as $responseDataItem) {
                        $this->assertJsonStructureExact($structure['*'], $responseDataItem);
                    }
                } elseif (is_array($value)) {
                    PHPUnit::assertArrayHasKey($key, $responseData);

                    $this->assertJsonStructureExact($structure[$key], $responseData[$key]);
                } else {
                    PHPUnit::assertArrayHasKey($value, $responseData);
                }
            }

            return $this;
        });
    }
}

Usage:


class MyTestCase extends TestCase
{
    use AssertJson;

    public function setUp(): void
    {
        parent::setUp();

        $this->setUpAssertJson();
    }

    public function text_example()
    {
        $this->get('api/users')->assertJsonStructureExact(['data' => ['*' => ['id', 'email']]]);
    }
}

Maybe, this post is closed and old, but I want shared the updated code for future users:

<?php
namespace Tests\Support;

use Illuminate\Foundation\Testing\TestResponse;
use Illuminate\Support\Arr;
use PHPUnit\Framework\Assert as PHPUnit;

trait AssertJson
{
    public function setUpAssertJson(): void
    {
        TestResponse::macro('assertJsonStructureExact', function (array $structure = null, $responseData = null) {
            if ($structure === null) {
                return $this->assertJson($this->json());
            }

            if ($responseData === null) {
                $responseData = $this->decodeResponseJson();
            }

            if (!array_key_exists('*', $structure)) {
                $keys = array_map(static function ($value, $key) {
                    return is_array($value) ? $key : $value;
                }, $structure, array_keys($structure));

                PHPUnit::assertEquals(Arr::sortRecursive($keys), Arr::sortRecursive(array_keys($responseData)));
            }

            foreach ($structure as $key => $value) {
                if (is_array($value) && $key === '*') {
                    PHPUnit::assertIsArray($responseData);

                    foreach ($responseData as $responseDataItem) {
                        $this->assertJsonStructureExact($structure['*'], $responseDataItem);
                    }
                } elseif (is_array($value)) {
                    PHPUnit::assertArrayHasKey($key, $responseData);

                    $this->assertJsonStructureExact($structure[$key], $responseData[$key]);
                } else {
                    PHPUnit::assertArrayHasKey($value, $responseData);
                }
            }

            return $this;
        });
    }
}

@ellisio
Copy link
Contributor

ellisio commented May 28, 2020

If you're using Laravel 7, TestResponse has moved. The new namespace is Illuminate\Testing\TestResponse.

@Oldenborg
Copy link

Oldenborg commented Sep 2, 2020

You can actually move the setup function into the trait itself.
I create a gist based on the example for anyone wanting to grab the code

https://gist.github.com/Oldenborg/dbea4eb6df6cabf388310e6b0168262f

@MohannadNaj
Copy link
Contributor

Thanks for the solutions here!

What do you think about this implementation?

use Illuminate\Support\Arr;
use Illuminate\Support\Str;
use Illuminate\Testing\TestResponse;
use Illuminate\Testing\Assert as PHPUnit;

        TestResponse::macro('assertJsonStructureExact', function(string $path, array $structure) {
            $structure = Arr::dot($structure);
            $responseStructure = array_keys(Arr::dot($this->decodeResponseJson()->json($path)));

            foreach ($structure as $key => $value) {
                $keyIndex = Str::afterLast($key, '.');

                if (! is_int(($keyIndex)))
                {
                    $newKey = Str::replaceLast($keyIndex, $value, $key);
                    unset($structure[$key]);
                    $structure[] = $newKey;
                }
            }

            PHPUnit::assertSame(Arr::sortRecursive($structure), Arr::sortRecursive($responseStructure));

            return $this;
        });

It uses similar logic (Manipulating into recursively sorted arrays) as in https://github.com/laravel/framework/blob/49ee16b9ee784d425205fa456b43581de7cf804a/src/Illuminate/Testing/AssertableJsonString.php

It also will accepts a $path parameter to determine where in the response we are looking for exact structure match.

@IllyaMoskvin
Copy link

Signal-boosting @Oldenborg's gist. There is an error with the first implementation after upgrading to Laravel 8:

ErrorException: array_keys() expects parameter 1 to be array, object given

You need to update this line to include json():

$responseData = $this->decodeResponseJson()->json();

However, I decided to not use this trait after all!

Instead, I'm using the each() method of AssertableJson.

Using the example in @brunocascio's original post, I think the true assertion would look like this:

$response->assertJson(fn ($json) => $json
    ->each(fn ($json) => $json
        ->hasAll(['id', 'name', 'slug'])
    )
);

Removing slug from that array should cause this assertion to fail.

Unfortunately, it seems that the each() method is not documented in "Fluent JSON Testing" on HTTP Tests... might be a useful addition, if anyone's interested in contributing to the docs!

@Oldenborg
Copy link

I know I'm a bit late to the party, I had no idea people were using this, until I randomly looked for a solution to this again today, and forgot that I made the gist....(feeling old..) I will get it updated to use with Laravel 10

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

No branches or pull requests

8 participants