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

feat: Driver with JSONPath support #191

Open
wants to merge 2 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
16 changes: 16 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -181,6 +181,22 @@ The `assertMatchesFileHashSnapshot($filePath)` assertion asserts that the hash o

The `assertMatchesFileSnapshot($filePath)` assertion works almost the same way as the file hash assertion, except that it actually saves the whole file in the snapshots directory. If the assertion fails, it places the failed file next to the snapshot file so they can easily be manually compared. The persisted failed file is automatically deleted when the test passes. This assertion is most useful when working with binary files that should be manually compared like images or pdfs.

### JSON snapshots

The `MatchesSnapshots` trait also offers two ways to assert on JSON files, usually helpful while working with API responses:

The `assertMatchesJsonSnapshot($actual)` method to assert that a JSON string is identical to the snapshot that was made the first time the test was run.

The `assertMatchesJsonPathSnapshot($actual, $placeholders)` method, which extends the static mechanism with a map of JSONPath and Regular Expressions to assert on specific, dynamic parts of the JSON string. This needs the additional package `galbar/jsonpath` to be installed.

```php
$this->assertMatchesJsonPathSnapshot($json, [
'$.id' => '@\d+@',
'$.cover' => '@https://bucket.foo/bar/\d+.[webp|jpg]@',
'$.createdAt' => '@\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}[\+\-]\d{2}:\d{2}@',
])
```

### Image snapshots

The `assertImageSnapshot` requires the [spatie/pixelmatch-php](https://github.com/spatie/pixelmatch-php) package to be installed.
Expand Down
4 changes: 3 additions & 1 deletion composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -31,11 +31,13 @@
"symfony/yaml": "^5.2|^6.2|^7.0"
},
"require-dev": {
"galbar/jsonpath": "^3.0",
"spatie/pixelmatch-php": "dev-main",
"spatie/ray": "^1.37"
},
"suggest": {
"spatie/pixelmatch-php": "Required to use the image snapshot assertions"
"spatie/pixelmatch-php": "Required to use the image snapshot assertions",
"galbar/jsonpath": "Required to use the JSONPath assertions"
},
"autoload": {
"psr-4": {
Expand Down
45 changes: 45 additions & 0 deletions src/Drivers/JsonPathDriver.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
<?php

namespace Spatie\Snapshots\Drivers;

use Exception;
use JsonPath\JsonObject;
use PHPUnit\Framework\Assert;

class JsonPathDriver extends JsonDriver
{
public function __construct(private readonly array $placeholders = [])
{
}

public function match($expected, $actual): void
{
if (! class_exists(JsonObject::class)) {
throw new Exception('The galbar/jsonpath package is not installed. Please install it to enable JSONPath driver.');
}

if (is_string($actual)) {
$actual = json_decode($actual, false, 512, JSON_THROW_ON_ERROR);
}

$expected = json_decode($expected, false, 512, JSON_THROW_ON_ERROR);

$jpActual = new JsonObject($actual);
$jpExpected = new JsonObject($expected);
foreach ($this->placeholders as $path => $pattern) {
$actualData = $jpActual->getJsonObjects($path);

if (0 === count($actualData)) {
Assert::fail('Failed asserting that JSON path "'.$path.'" exists.');
}

$expectedData = $jpExpected->getJsonObjects($path);
foreach ($actualData as $i => $data) {
Assert::assertMatchesRegularExpression($pattern, $data->getValue(), 'Failed asserting that JSON path "'.$path.'" matches pattern "'.$pattern.'".');
$data->set('$', $expectedData[$i]->getValue());
}
}

Assert::assertJsonStringEqualsJsonString($jpExpected->getJson(), $jpActual->getJson());
}
}
6 changes: 6 additions & 0 deletions src/MatchesSnapshots.php
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
use Spatie\Snapshots\Drivers\HtmlDriver;
use Spatie\Snapshots\Drivers\ImageDriver;
use Spatie\Snapshots\Drivers\JsonDriver;
use Spatie\Snapshots\Drivers\JsonPathDriver;
use Spatie\Snapshots\Drivers\ObjectDriver;
use Spatie\Snapshots\Drivers\TextDriver;
use Spatie\Snapshots\Drivers\XmlDriver;
Expand Down Expand Up @@ -94,6 +95,11 @@ public function assertMatchesJsonSnapshot(array|string|null|int|float|bool $actu
$this->assertMatchesSnapshot($actual, new JsonDriver());
}

public function assertMatchesJsonPathSnapshot(array|string|null|int|float|bool $actual, array $placeholders): void
{
$this->assertMatchesSnapshot($actual, new JsonPathDriver($placeholders));
}

public function assertMatchesObjectSnapshot($actual): void
{
$this->assertMatchesSnapshot($actual, new ObjectDriver());
Expand Down
60 changes: 60 additions & 0 deletions tests/Unit/Drivers/JsonPathDriverTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
<?php

declare(strict_types=1);

namespace Spatie\Snapshots\Test\Unit\Drivers;

use Generator;
use JsonPath\JsonObject;
use JsonPath\JsonPath;
use PHPUnit\Framework\Attributes\DataProvider;
use PHPUnit\Framework\Attributes\Test;
use PHPUnit\Framework\ExpectationFailedException;
use PHPUnit\Framework\TestCase;
use Spatie\Snapshots\Drivers\JsonPathDriver;

class JsonPathDriverTest extends TestCase
{
#[Test]
#[DataProvider('provideJsonData')]
public function it_can_replace_placeholders_in_json(string $pathExpected, string $pathActual, array $replacements): void
{
$expected = file_get_contents($pathExpected);
$actual = file_get_contents($pathActual);
$driver = new JsonPathDriver($replacements);

try {
$driver->match($expected, $actual);
$status = true;
} catch (ExpectationFailedException $e) {
print(PHP_EOL.PHP_EOL.$e->getMessage().PHP_EOL.PHP_EOL);
$status = false;
}

$this->assertTrue($status);
}

public static function provideJsonData(): Generator
{
yield 'simple' => [
dirname(__DIR__).'/test_files/json_path_simpleA.json',
dirname(__DIR__).'/test_files/json_path_simpleB.json',
[
'$.id' => '@\d+@',
'$.cover' => '@https://bucket.foo/bar/\d+.[webp|jpg]@',
'$.createdAt' => '@\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}[\+\-]\d{2}:\d{2}@',
]
];

yield 'json:api' => [
dirname(__DIR__).'/test_files/json_path_jsonapiA.json',
dirname(__DIR__).'/test_files/json_path_jsonapiB.json',
[
'$.data..id' => '@\d+@',
'$.data..links.*' => '@http://example.com/articles/\d+(/[a-z/]+)?@',
'$.included..id' => '@\d+@',
'$.included..links.self' => '@http://example.com/(people|comments)/\d+@',
]
];
}
}
76 changes: 76 additions & 0 deletions tests/Unit/test_files/json_path_jsonapiA.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
{
"links": {
"self": "http://example.com/articles",
"next": "http://example.com/articles?page[offset]=2",
"last": "http://example.com/articles?page[offset]=10"
},
"data": [{
"type": "articles",
"id": "1",
"attributes": {
"title": "JSON:API paints my bikeshed!"
},
"relationships": {
"author": {
"links": {
"self": "http://example.com/articles/1/relationships/author",
"related": "http://example.com/articles/1/author"
},
"data": { "type": "people", "id": "9" }
},
"comments": {
"links": {
"self": "http://example.com/articles/1/relationships/comments",
"related": "http://example.com/articles/1/comments"
},
"data": [
{ "type": "comments", "id": "5" },
{ "type": "comments", "id": "12" }
]
}
},
"links": {
"self": "http://example.com/articles/1"
}
}],
"included": [{
"type": "people",
"id": "9",
"attributes": {
"firstName": "Dan",
"lastName": "Gebhardt",
"twitter": "dgeb"
},
"links": {
"self": "http://example.com/people/9"
}
}, {
"type": "comments",
"id": "5",
"attributes": {
"body": "First!"
},
"relationships": {
"author": {
"data": { "type": "people", "id": "2" }
}
},
"links": {
"self": "http://example.com/comments/5"
}
}, {
"type": "comments",
"id": "12",
"attributes": {
"body": "I like XML better"
},
"relationships": {
"author": {
"data": { "type": "people", "id": "9" }
}
},
"links": {
"self": "http://example.com/comments/12"
}
}]
}
76 changes: 76 additions & 0 deletions tests/Unit/test_files/json_path_jsonapiB.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
{
"links": {
"self": "http://example.com/articles",
"next": "http://example.com/articles?page[offset]=2",
"last": "http://example.com/articles?page[offset]=10"
},
"data": [{
"type": "articles",
"id": "2",
"attributes": {
"title": "JSON:API paints my bikeshed!"
},
"relationships": {
"author": {
"links": {
"self": "http://example.com/articles/2/relationships/author",
"related": "http://example.com/articles/2/author"
},
"data": { "type": "people", "id": "4" }
},
"comments": {
"links": {
"self": "http://example.com/articles/1/relationships/comments",
"related": "http://example.com/articles/1/comments"
},
"data": [
{ "type": "comments", "id": "7" },
{ "type": "comments", "id": "18" }
]
}
},
"links": {
"self": "http://example.com/articles/2"
}
}],
"included": [{
"type": "people",
"id": "4",
"attributes": {
"firstName": "Dan",
"lastName": "Gebhardt",
"twitter": "dgeb"
},
"links": {
"self": "http://example.com/people/4"
}
}, {
"type": "comments",
"id": "7",
"attributes": {
"body": "First!"
},
"relationships": {
"author": {
"data": { "type": "people", "id": "22" }
}
},
"links": {
"self": "http://example.com/comments/5"
}
}, {
"type": "comments",
"id": "18",
"attributes": {
"body": "I like XML better"
},
"relationships": {
"author": {
"data": { "type": "people", "id": "99" }
}
},
"links": {
"self": "http://example.com/comments/18"
}
}]
}
10 changes: 10 additions & 0 deletions tests/Unit/test_files/json_path_simpleA.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
{
"id": 123456,
"title": "Some Title",
"author": "Jane Doe",
"ISBN": "978-0-545-01022-1",
"cover": "https://bucket.foo/bar/123456.webp",
"published": 2017,
"createdAt": "2024-03-08T17:37:09+00:00",
"more": "values"
}
10 changes: 10 additions & 0 deletions tests/Unit/test_files/json_path_simpleB.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
{
"id": 987654,
"title": "Some Title",
"author": "Jane Doe",
"ISBN": "978-0-545-01022-1",
"cover": "https://bucket.foo/bar/987654.webp",
"published": 2017,
"createdAt": "2024-04-02T22:33:44+55:11",
"more": "values"
}