Skip to content

Instantly share code, notes, and snippets.

@Oldenborg
Last active October 2, 2023 22:50
Show Gist options
  • Star 3 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save Oldenborg/dbea4eb6df6cabf388310e6b0168262f to your computer and use it in GitHub Desktop.
Save Oldenborg/dbea4eb6df6cabf388310e6b0168262f to your computer and use it in GitHub Desktop.
Laravel test trait: assert JSON exact
<?php
namespace Tests\Traits;
use Illuminate\Testing\TestResponse;
use Illuminate\Support\Arr;
use PHPUnit\Framework\Assert as PHPUnit;
trait AssertJson
{
/* @before */
public function setUp(): void
{
parent::setUp();
$this->setUpAssertJson();
}
public function setUpAssertJson(): void
{
TestResponse::macro('assertJsonStructureExact', function (array $structure = null, $responseData = null) {
$transformStructure = function ($structure, $prefix = '') use (&$transformStructure) {
$result = [];
foreach ($structure as $key => $value) {
if (is_array($value) && array_keys($value) !== range(0, count($value) - 1)) {
$result = array_merge($result, $transformStructure($value, $prefix . $key . '.'));
} elseif (is_array($value)) {
foreach ($value as $k) {
$result[] = $prefix . $key . '.' . $k;
}
} else {
$result[] = $prefix . $value;
}
}
return $result;
};
$expectedKeys = $transformStructure($structure);
sort($expectedKeys);
if ($responseData === null) {
$responseData = json_decode($this->getContent(), true);
}
$responseKeys = Arr::dot($responseData);
$responseKeys = array_keys($responseKeys);
sort($responseKeys);
$missingKeys = array_diff($expectedKeys, $responseKeys);
$extraKeys = array_diff($responseKeys, $expectedKeys);
$errorMessage = "";
if (!empty($extraKeys)) {
$prettyList = "[\n '" . implode("',\n '", $extraKeys) . "'\n]";
$errorMessage .= "The response had the following unexpected parameters:\n" . $prettyList . ".";
}
if (!empty($missingKeys)) {
$prettyList = "[\n '" . implode("',\n '", $missingKeys) . "'\n]";
$errorMessage .= "The response is missing the following parameters:\n" . $prettyList . ".";
}
PHPUnit::assertTrue(empty($missingKeys) && empty($extraKeys), $errorMessage);
return $this;
});
}
}
@Oldenborg
Copy link
Author

How to use this trait.

  1. place the file in the tests folder of your laravel project tests\AssertJson.php
  2. Use the AssertJson Trait inside your test.

example

<?php

namespace Tests\Feature;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Tests\AssertJson;
use Tests\TestCase;

class UserFeatureTest extends TestCase
{
    use RefreshDatabase, AssertJson;

    /** @test */
    public function list_competitions() {

        $user = factory(App\User::class)->make();

        $response = $this->get('/api/users/{$user->id}');
        $response
            ->assertStatus(200)
            ->assertJson(['data' => [
                'id' => $user->id,
            ]])
            ->assertJsonStructureExact([ 'data' => [
                'id',
                'name',
            ]]);
    }
}

@Oldenborg
Copy link
Author

Thanks to @dmitrybubyakin and @MrJmpl3 for the initial code solution

@Darkproduct
Copy link

Darkproduct commented Sep 4, 2020

Not sure what I'm doing wrong. I get

1) Tests\Feature\OpticalReaderModelTest::testIndex
BadMethodCallException: Method Illuminate\Http\JsonResponse::assertJsonStructureExact does not exist.

Illuminate/Support/Traits/Macroable.php:103
Illuminate/Testing/TestResponse.php:1327
tests/Feature/OpticalReaderModelTest.php:36

assertJsonStructure would work fine there, but has the downside of this.

Edit:
I found my problem. My TestCase Class has its own setUp method, which overrides the setUp from the trait. So I add $this->setUpAssertJson(); to my setUp method and now everything works fine.

@Darkproduct
Copy link

Darkproduct commented Sep 19, 2020

After upgrading to Laravel 8, I got this error from previously (in Laravel 7) working tests.

  1. Tests\Feature\MyTest::testIndex
    ErrorException: array_keys() expects parameter 1 to be array, object given

tests/AssertJson.php:37
vendor/laravel/framework/src/Illuminate/Macroable/Traits/Macroable.php:111
vendor/laravel/framework/src/Illuminate/Testing/TestResponse.php:1215
tests/Feature/MyTest.php:28

The breaking change is in:

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

decodeResponseJson() will now return an object with the original json and the decoded array. Unfortunately, the decoded array is protected and not accessible from this trait.

Here is how to fix this: (Line 25-27)

if ($responseData === null) {
    $responseData = json_decode($this->decodeResponseJson()->json, true);
}

@IllyaMoskvin
Copy link

I had success fixing it by doing this, instead of calling json_decode():

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

Not sure when exactly the json() method was introduced.

@Oldenborg
Copy link
Author

I had success fixing it by doing this, instead of calling json_decode():

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

Not sure when exactly the json() method was introduced.

I havent look at this trait for a long time. And I haven't been doing much Laravel development the past few years. @IllyaMoskvin do you suggest I update my Trait to reflect this change to $responseData = $this->decodeResponseJson()->json();?

@Oldenborg
Copy link
Author

Oldenborg commented Oct 2, 2023

I have finally taken the time to update this Trait so it works with the lastest version of Laravel (10)

At the same time, I have implemented more meaningful error messages that will be visible in the console.

   FAILED  Tests\Feature\AuthenticationTest > a guest can register                          
  The response had the following unexpected parameters:
[
    'data.token',
    'data.user.created_at',
    'data.user.email_verification_token',
    'data.user.updated_at'
],
The response is missing the following parameters:
[
    'status'
].

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment