talentia's avatar

How to assert that the database contains a value stored as JSON

Within my PHPUnit test I'm trying to assert that an API endpoint properly stores the data sent to it. In my database I have a field called settings which stores data using the type: JSON. Everything works just fine. I just can't get PHPUnit to accept the assertion, despite the values visually appearing the same.

I'm using PHPUnit version 5.7.

$data = [
    'user_id'  => 1,
    'settings' => '{"foo": "bar"}'
];

$this->post('user_settings', $data);

$this->assertDatabaseHas('user_settings', [
    'id'       => 1,
    'user_id'  => 1,
    'settings' => '{"foo": "bar"}'
]);
Failed asserting that a row in the table [user_settings] matches the attributes {
    "id": 1,
    "settings": "{\"foo\": \"bar\"}"
}.

Found: [
    {
        "id": 1,
        "user_id": 1,
        "settings": "{\"foo\": \"bar\"}",
        "created_at": "2017-07-22 17:04:35",
        "updated_at": "2017-07-22 17:04:35",
        "deleted_at": null
    }
].

Any ideas why this would fail? Does PHPUnit just not like JSON database fields?

0 likes
15 replies
Vitaly's avatar

The same problem, but i resolved it another way

$userSettings = UserSettings::find(1);
$this->assertEquals(['foo' => 'bar'], $userSettings->settings);
4 likes
riabiy.denis's avatar

Are there any better solutions? Looks like a duct tape.

2 likes
talentia's avatar
talentia
OP
Best Answer
Level 9

You have to first cast the value to JSON to be able to search for it in MySQL. I use the following helper method to do this:

/**
 * Generate a raw DB query to search for a JSON field.
 *
 * @param  array|json  $json
 *
 * @throws \Exception
 *
 * @return \Illuminate\Database\Query\Builder
 */
function castToJson($json)
{
    // Convert from array to json and add slashes, if necessary.
    if (is_array($json)) {
        $json = addslashes(json_encode($json));
    }
    // Or check if the value is malformed.
    elseif (is_null($json) || is_null(json_decode($json))) {
        throw new \Exception('A valid JSON string was not provided.');
    }
    return \DB::raw("CAST('{$json}' AS JSON)");
}

So, using my original example the database assertion would become:

$this->assertDatabaseHas('user_settings', [
    'id'       => 1,
    'user_id'  => 1,
    'settings' => castToJson('{"foo": "bar"}'),
]);

Hope that helps!

20 likes
Artistan's avatar

Thanks @talentia.

Here is a "simple" test i did with json that had factories involved...


$Location = factory(App\Models\Location::class)->make();
$Edited = factory(App\Models\Location::class)->make();
// create item
$this->actor->call('POST', 'locations', $Location->toArray());
// test edit
$response = $this->actor->call('PATCH', 'locations/1', $Edited->toArray());
// assert edited data exists...

        // assert Database Has does not handle json well...
        $test = $Edited->toArray();
        $test['json_data'] = castToJson($test['json_data']);
        $this->assertDatabaseHas('locations', $test);
Sprov03's avatar

    /**
     * @test
     */
    public function canUpdate()
    {
        $authUser = UserFactory::createDefault();
        $this->actingAs($authUser);

        $generator = GeneratorFactory::createDefault();

        $request = [
            'requires_data' => true,
            'documentation' => "99999",
            'prompters' => [
                'array-data',
                ['more-data' => 'cool'],
                'data' => 'some-data',
                'collectionA' => [
                    ['not-working' => 'cant-access'],
                    'data' => 'some-more-data'
                ],
                'collection' => [
                    ['data' => 'some-more-data']
                ]
            ],
            'file_modifiers' => [],
            'template_ids' => []
        ];

        $response = $this->putJson("/api/generators/{$generator->id}", $request);
        $response->assertOk();

        $this->assertDatabaseHas('generators', [
            'id' => $generator->id,
            'generator_set_id' => $generator->generatorSet->id,
            'requires_data' => true,
            'documentation' => 99999,
            'prompters->0' => 'array-data',
            'prompters->1->more-data' => 'cool',
            'prompters->data' => 'some-data',
            'prompters->collectionA->data' => 'some-more-data',
            'prompters->collectionA->0->not-working' => 'cant-access',
            'file_modifiers->0' => null,
        ]);
    }
5 likes
keizah7's avatar

@talentia @riabiy.denis @vitaly I know it is old question, but today I encountered same problem and my fast solution was:

/** @test */
function can_create_translatable_category()
{
//        $category = Category::factory()->raw();
    $category = Category::factory()->raw(
        [
            'title' => [
                'lt' => 'Darbas',
            ],
            'slug' => [
                'lt' => 'darbas',
            ]
        ]
    );

    $this->signIn(guard: 'admin')
        ->post(route('dashboard.admin.category.store'), $category)
        ->assertRedirect();

    $this->assertDatabaseHasJson('categories', $category);
}

TestCase.php

protected function assertDatabaseHasJson(string $table, array $data): TestCase
{
    collect($data)
        ->map(function ($item, $property) use (&$data) {
            if (is_array($item)) {
                collect($item)
                    ->map(function ($value, $key) use ($property, &$data) {
                        $data["$property->$key"] = $value;
                    });
                unset($data[$property]);
            }
        })
        ->toArray();

    return $this->assertDatabaseHas($table, $data);
}
gauravmak's avatar

Using json_encode on the value worked for me:

$this->assertDatabaseHas('users', [
    'name' => 'Gaurav',
    'attributes' => json_encode([
        'gender' => 'Male',
        'nationality' => 'Indian',
    ]),
]);
1 like
zachvv11's avatar

The base Laravel Illuminate\Foundation\Testing\TestCase class now includes a castAsJson method:

$this->assertDatabaseHas('users', [
    'name' => 'my name',
    'metadata' => $this->castAsJson([
        'strength' => 12,
        'color' => 'red',
    ]),
]);
26 likes
ZejdCicak's avatar

@zachvv11 I have been having an SQL error SQLSTATE[42883]: Undefined function: 7 ERROR: operator does not exist: json = json even after using castAsJson any idea why that might be happening?

1 like
RonnyKnoxville's avatar

If you are using json fields in your migrations:

$table->json('my_json_field');

And casting your JSON field in the model:

protected $casts = [
    "my_json_field" => "array",
];

Then a simplified version of the answer above should work:

protected function assertDatabaseHasJson(string $table, array $data)
{
    $dataArray = collect($data)->map(function ($item) {
        return is_array($item) ? $this->castAsJson($item) : $item;
    })->toArray();

    return $this->assertDatabaseHas($table, $dataArray);
}

Please or to participate in this conversation.