Skip to content

Instantly share code, notes, and snippets.

@DominikStyp
Created October 25, 2022 18:28
Show Gist options
  • Save DominikStyp/8df177688ec1598984128d59fc63797b to your computer and use it in GitHub Desktop.
Save DominikStyp/8df177688ec1598984128d59fc63797b to your computer and use it in GitHub Desktop.
PHPUnit: Asserting JSON Paths
<?php
namespace Tests\Http;
/**
*
* Use this trait in your Laravel/Lumen TestCase class
* to get the assertJSON... functionality out of the box
*
* Trait CanAssertJsonPaths
* @package Tests\Http
*/
trait CanAssertJsonPaths
{
protected static $uriCachedData = [];
/**
* Visit the given URI with a JSON request.
*
* @param string $uri - endpoint uri ex. '/api/user/1'
* @param array $data
* @param array $headers
* @return $this
*/
public function cachedJsonGet(string $uri, array $data = [], array $headers = [])
{
if (empty(static::$uriCachedData[$uri])) {
$this->json('GET', $uri, $data, $headers);
static::$uriCachedData[$uri] = $this->response;
return $this;
} else {
$this->response = static::$uriCachedData[$uri];
return $this;
}
}
public function clearCacheForUri(string $uri)
{
if(!empty(static::$uriCachedData[$uri])){
unset(static::$uriCachedData[$uri]);
}
}
/**
* @param string $uri - endpoint uri ex. '/api/user/1'
* @param bool $cached
* @return false|string
*/
protected function getContentResponseFromUri(string $uri, bool $cached = false)
{
if ($cached) {
$response = $this->cachedJsonGet($uri)->response->getContent();
} else {
$response = $this->json('GET', $uri)->response->getContent();
}
return $response;
}
/**
* @param string $uri - endpoint uri ex. '/api/user/1'
* @param string $jsonPath - ex. 'data.users[1].name'
* @param bool $cached - should endpoint result be cached across tests
* @throws \JsonException
*/
protected function assertJsonPathExists(string $uri, string $jsonPath, bool $cached = false)
{
$jsonPath = $this->clearJSONPathAndChangeItToPHPSyntax($jsonPath);
$content = $this->getContentResponseFromUri($uri, $cached);
$contentDecoded = json_decode($content, false);
$pathToCheck = '$contentDecoded->' . $jsonPath;
$isset = eval('return isset(' . $pathToCheck . ');');
$this->assertTrue($isset, "Error in endpoint: {$uri} \n {$pathToCheck} path doesn't exist in JSON");
}
/**
* @param string $uri - endpoint uri ex. '/api/user/1'
* @param string $jsonPath - ex. 'data.users[1].name'
* @param bool $cached - should endpoint result be cached across tests
* @throws \JsonException
*/
protected function assertJsonPathDoesntExist(string $uri, string $jsonPath, bool $cached = false)
{
$jsonPath = $this->clearJSONPathAndChangeItToPHPSyntax($jsonPath);
$content = $this->getContentResponseFromUri($uri, $cached);
$contentDecoded = json_decode($content, false);
$pathToCheck = '$contentDecoded->' . $jsonPath;
$isNotSet = eval('return !isset(' . $pathToCheck . ');');
$valueOfTheKey = "";
if (!$isNotSet) {
$valueOfTheKey = eval('return json_encode(' . $pathToCheck . ');');
}
$this->assertTrue($isNotSet,
"Error in endpoint: {$uri} \n{$pathToCheck}path exists in JSON, value is: {$valueOfTheKey}");
}
/**
* Is empty means: [], null, 0, "", false, !isset()
*
* @param string $uri - endpoint uri ex. '/api/user/1'
* @param string $jsonPath - ex. 'data.users[1].name'
* @param bool $cached - should endpoint result be cached across tests in class (first invoke is stored in cache
* and served to the rest invokes)
*/
protected function assertJsonPathIsEmpty(string $uri, string $jsonPath, bool $cached = false)
{
$jsonPath = $this->clearJSONPathAndChangeItToPHPSyntax($jsonPath);
$content = $this->getContentResponseFromUri($uri, $cached);
$contentDecoded = json_decode($content, false);
$pathToCheck = '$contentDecoded->' . $jsonPath;
$empty = eval('return empty(' . $pathToCheck . ');');
$valueOfTheKey = "";
if (!$empty) {
$valueOfTheKey = eval('return json_encode(' . $pathToCheck . ');');
}
$this->assertTrue($empty,
"Error in endpoint: {$uri} \n{$pathToCheck} path is not empty, so does not equal: [], null, 0, \"\", false, or !isset() \nValue of the key: {$valueOfTheKey}");
}
/**
* @param string $uri - endpoint uri ex. '/api/user/1'
* @param string $jsonPath - ex. 'data.users[1].name'
* @param string|int|float $expectedValue
* @param bool $cached - should endpoint result be cached across tests in class (first invoke is stored in cache
* and served to the rest invokes)
* @throws \JsonException
*/
protected function assertJsonPathHasValue(string $uri, string $jsonPath, $expectedValue, bool $cached = false)
{
$this->assertJsonPathExists($uri, $jsonPath, $cached);
$content = $this->getContentResponseFromUri($uri, $cached);
$contentDecoded = json_decode($content, false);
$jsonPath = $this->clearJSONPathAndChangeItToPHPSyntax($jsonPath);
$pathToCheck = '$contentDecoded->' . $jsonPath;
$value = eval('return ' . $pathToCheck . ';');
if (is_string($expectedValue) && preg_replace("#\s+#", "", $expectedValue) === '[]') {
$this->assertTrue(is_array($value), "{$pathToCheck} path is not an array");
} else {
$this->assertEquals($expectedValue, $value,
"Error in endpoint: {$uri} \n{$pathToCheck} path doesn't have expected value: {$expectedValue}, instead it equals: {$value}");
}
}
/**
* @param string $uri - endpoint uri ex. '/api/user/1'
* @param string $jsonPath - ex. 'data.users[1].name'
* @param string $expectedRegex - ex. '#\d+#' ( # is regex delimiter in the example )
* @param bool $cached - should endpoint result be cached across tests in class (first invoke is stored in cache
* and served to the rest invokes)
* @throws \JsonException
*/
protected function assertJsonPathHasRegex(string $uri, string $jsonPath, string $expectedRegex, bool $cached = false)
{
$this->assertJsonPathExists($uri, $jsonPath, $cached);
$content = $this->getContentResponseFromUri($uri, $cached);
$contentDecoded = json_decode($content, false);
$jsonPath = $this->clearJSONPathAndChangeItToPHPSyntax($jsonPath);
$pathToCheck = '$contentDecoded->' . $jsonPath;
$value = eval('return ' . $pathToCheck . ';');
if (preg_replace("#\s+#", "", $expectedRegex) === '[]') {
$this->assertTrue(is_array($value), "$pathToCheck path is not an array");
} else {
$this->assertRegExp($expectedRegex, strval($value),
"Error in endpoint: {$uri} \n$pathToCheck path doesn't have expected value: {$expectedRegex}, instead it equals: {$value}");
}
}
/**
* @param string $jsonPath - ex. 'data.users[1].name'
* @return string
*/
private function clearJSONPathAndChangeItToPHPSyntax(string $jsonPath): string
{
// clear from JSON not-allowed chars
$jsonPath = preg_replace('#[^a-zA-Z0-9\"\]\[._ -]+#', '', $jsonPath);
// replace 'data.user[1].name' to 'data->user[1]->name' which is valid PHP syntax
$jsonPath = str_replace('.', '->', $jsonPath);
return $jsonPath;
}
}
<?php
namespace Tests\Http;
class CatalogEndpointTest extends TestCase
{
public function testCatalogBrowser()
{
$uri = '/api/v1/catalog/browser?category_slug=some-slug';
$this->assertJsonPathHasValue($uri, 'data.items[0].product.alias', 'Training');
$this->assertJsonPathHasValue($uri, 'data.items[1].product.alias', 'Training');
$this->assertJsonPathHasRegex($uri, 'data.items[0].product.alias.id', self::UUID_REGEX, true);
}
}
<?php
namespace Tests\Http;
abstract class TestCase extends \Laravel\Lumen\Testing\TestCase
{
use CanAssertJsonPaths;
// ...
// ...
const ID_REGEXP = "#[A-Z0-9-]{36}#";
const DATE_TIME_REGEX = "#\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}#";
const CURRENCY_CODE_REGEXP = "#[A-Z]{2,5}#";
const ALIAS_REGEX = '#[A-Za-z0-9-]{3,}#si';
const TITLE_REGEX = "#\w{3,}#";
const UUID_REGEX = "#[A-Z0-9-]{20,}#";
const SLUG_REGEX = '#[a-zA-Z0-9_-]+#';
const PRODUCT_CODE_REGEX = "/[A-Za-z0-9#-_]{2,}.*?/";
const FLOAT_REGEX = '#^[0-9.]{1,}$#';
const INT_REGEX = '#^[0-9]{1,}$#';
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment