Skip to content

Commit

Permalink
[8.x] Adjust Fluent Assertions (#36620)
Browse files Browse the repository at this point in the history
* Adjust Fluent Assertions

- Add `first` scoping method
- Allow `has(3)` to count the current scope
- Expose `count` method publicly

* Skip interaction check when top-level is non-associative

* Test: Remove redundant `etc` call

* Fix broken test
  • Loading branch information
claudiodekker authored Mar 16, 2021
1 parent 354c57b commit a7ec58e
Show file tree
Hide file tree
Showing 6 changed files with 242 additions and 36 deletions.
28 changes: 26 additions & 2 deletions src/Illuminate/Testing/Fluent/AssertableJson.php
Original file line number Diff line number Diff line change
Expand Up @@ -52,13 +52,13 @@ protected function __construct(array $props, string $path = null)
* @param string $key
* @return string
*/
protected function dotPath(string $key): string
protected function dotPath(string $key = ''): string
{
if (is_null($this->path)) {
return $key;
}

return implode('.', [$this->path, $key]);
return rtrim(implode('.', [$this->path, $key]), '.');
}

/**
Expand Down Expand Up @@ -93,6 +93,30 @@ protected function scope(string $key, Closure $callback): self
return $this;
}

/**
* Instantiate a new "scope" on the first child element.
*
* @param \Closure $callback
* @return $this
*/
public function first(Closure $callback): self
{
$props = $this->prop();

$path = $this->dotPath();

PHPUnit::assertNotEmpty($props, $path === ''
? 'Cannot scope directly onto the first element of the root level because it is empty.'
: sprintf('Cannot scope directly onto the first element of property [%s] because it is empty.', $path)
);

$key = array_keys($props)[0];

$this->interactsWith($key);

return $this->scope($key, $callback);
}

/**
* Create a new instance from an array.
*
Expand Down
76 changes: 52 additions & 24 deletions src/Illuminate/Testing/Fluent/Concerns/Has.php
Original file line number Diff line number Diff line change
Expand Up @@ -11,12 +11,26 @@ trait Has
/**
* Assert that the prop is of the expected size.
*
* @param string $key
* @param int $length
* @param string|int $key
* @param int|null $length
* @return $this
*/
protected function count(string $key, int $length): self
public function count($key, int $length = null): self
{
if (is_null($length)) {
$path = $this->dotPath();

PHPUnit::assertCount(
$key,
$this->prop(),
$path
? sprintf('Property [%s] does not have the expected size.', $path)
: sprintf('Root level does not have the expected size.')
);

return $this;
}

PHPUnit::assertCount(
$length,
$this->prop($key),
Expand All @@ -29,41 +43,40 @@ protected function count(string $key, int $length): self
/**
* Ensure that the given prop exists.
*
* @param string $key
* @param null $value
* @param \Closure|null $scope
* @param string|int $key
* @param int|\Closure|null $length
* @param \Closure|null $callback
* @return $this
*/
public function has(string $key, $value = null, Closure $scope = null): self
public function has($key, $length = null, Closure $callback = null): self
{
$prop = $this->prop();

if (is_int($key) && is_null($length)) {
return $this->count($key);
}

PHPUnit::assertTrue(
Arr::has($prop, $key),
sprintf('Property [%s] does not exist.', $this->dotPath($key))
);

$this->interactsWith($key);

// When all three arguments are provided this indicates a short-hand expression
// that combines both a `count`-assertion, followed by directly creating the
// `scope` on the first element. We can simply handle this correctly here.
if (is_int($value) && ! is_null($scope)) {
$prop = $this->prop($key);
$path = $this->dotPath($key);

PHPUnit::assertTrue($value > 0, sprintf('Cannot scope directly onto the first entry of property [%s] when asserting that it has a size of 0.', $path));
PHPUnit::assertIsArray($prop, sprintf('Direct scoping is unsupported for non-array like properties such as [%s].', $path));

$this->count($key, $value);
if (is_int($length) && ! is_null($callback)) {
return $this->has($key, function (self $scope) use ($length, $callback) {
return $scope->count($length)
->first($callback)
->etc();
});
}

return $this->scope($key.'.'.array_keys($prop)[0], $scope);
if (is_callable($length)) {
return $this->scope($key, $length);
}

if (is_callable($value)) {
$this->scope($key, $value);
} elseif (! is_null($value)) {
$this->count($key, $value);
if (! is_null($length)) {
return $this->count($key, $length);
}

return $this;
Expand Down Expand Up @@ -129,7 +142,7 @@ public function missing(string $key): self
* @param string $key
* @return string
*/
abstract protected function dotPath(string $key): string;
abstract protected function dotPath(string $key = ''): string;

/**
* Marks the property as interacted.
Expand All @@ -155,4 +168,19 @@ abstract protected function prop(string $key = null);
* @return $this
*/
abstract protected function scope(string $key, Closure $callback);

/**
* Disables the interaction check.
*
* @return $this
*/
abstract public function etc();

/**
* Instantiate a new "scope" on the first element.
*
* @param \Closure $callback
* @return $this
*/
abstract public function first(Closure $callback);
}
2 changes: 1 addition & 1 deletion src/Illuminate/Testing/Fluent/Concerns/Matching.php
Original file line number Diff line number Diff line change
Expand Up @@ -87,7 +87,7 @@ protected function ensureSorted(&$value): void
* @param string $key
* @return string
*/
abstract protected function dotPath(string $key): string;
abstract protected function dotPath(string $key = ''): string;

/**
* Ensure that the given prop exists.
Expand Down
2 changes: 1 addition & 1 deletion src/Illuminate/Testing/TestResponse.php
Original file line number Diff line number Diff line change
Expand Up @@ -523,7 +523,7 @@ public function assertJson($value, $strict = false)

$value($assert);

if ($strict) {
if (Arr::isAssoc($assert->toArray())) {
$assert->interacted();
}
}
Expand Down
150 changes: 146 additions & 4 deletions tests/Testing/Fluent/AssertTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,7 @@ public function testAssertHasFailsWhenNestedPropMissing()
$assert->has('example.another');
}

public function testAssertCountItemsInProp()
public function testAssertHasCountItemsInProp()
{
$assert = AssertableJson::fromArray([
'bar' => [
Expand All @@ -70,7 +70,7 @@ public function testAssertCountItemsInProp()
$assert->has('bar', 2);
}

public function testAssertCountFailsWhenAmountOfItemsDoesNotMatch()
public function testAssertHasCountFailsWhenAmountOfItemsDoesNotMatch()
{
$assert = AssertableJson::fromArray([
'bar' => [
Expand All @@ -85,7 +85,7 @@ public function testAssertCountFailsWhenAmountOfItemsDoesNotMatch()
$assert->has('bar', 1);
}

public function testAssertCountFailsWhenPropMissing()
public function testAssertHasCountFailsWhenPropMissing()
{
$assert = AssertableJson::fromArray([
'bar' => [
Expand All @@ -111,6 +111,90 @@ public function testAssertHasFailsWhenSecondArgumentUnsupportedType()
$assert->has('bar', 'invalid');
}

public function testAssertHasOnlyCounts()
{
$assert = AssertableJson::fromArray([
'foo',
'bar',
'baz',
]);

$assert->has(3);
}

public function testAssertHasOnlyCountFails()
{
$assert = AssertableJson::fromArray([
'foo',
'bar',
'baz',
]);

$this->expectException(AssertionFailedError::class);
$this->expectExceptionMessage('Root level does not have the expected size.');

$assert->has(2);
}

public function testAssertHasOnlyCountFailsScoped()
{
$assert = AssertableJson::fromArray([
'bar' => [
'baz' => 'example',
'prop' => 'value',
],
]);

$this->expectException(AssertionFailedError::class);
$this->expectExceptionMessage('Property [bar] does not have the expected size.');

$assert->has('bar', function ($bar) {
$bar->has(3);
});
}

public function testAssertCount()
{
$assert = AssertableJson::fromArray([
'foo',
'bar',
'baz',
]);

$assert->count(3);
}

public function testAssertCountFails()
{
$assert = AssertableJson::fromArray([
'foo',
'bar',
'baz',
]);

$this->expectException(AssertionFailedError::class);
$this->expectExceptionMessage('Root level does not have the expected size.');

$assert->count(2);
}

public function testAssertCountFailsScoped()
{
$assert = AssertableJson::fromArray([
'bar' => [
'baz' => 'example',
'prop' => 'value',
],
]);

$this->expectException(AssertionFailedError::class);
$this->expectExceptionMessage('Property [bar] does not have the expected size.');

$assert->has('bar', function ($bar) {
$bar->count(3);
});
}

public function testAssertMissing()
{
$assert = AssertableJson::fromArray([
Expand Down Expand Up @@ -421,7 +505,7 @@ public function testScopeShorthandFailsWhenAssertingZeroItems()
]);

$this->expectException(AssertionFailedError::class);
$this->expectExceptionMessage('Cannot scope directly onto the first entry of property [bar] when asserting that it has a size of 0.');
$this->expectExceptionMessage('Property [bar] does not have the expected size.');

$assert->has('bar', 0, function (AssertableJson $item) {
$item->where('key', 'first');
Expand All @@ -445,6 +529,64 @@ public function testScopeShorthandFailsWhenAmountOfItemsDoesNotMatch()
});
}

public function testFirstScope()
{
$assert = AssertableJson::fromArray([
'foo' => [
'key' => 'first',
],
'bar' => [
'key' => 'second',
],
]);

$assert->first(function (AssertableJson $item) {
$item->where('key', 'first');
});
}

public function testFirstScopeFailsWhenNoProps()
{
$assert = AssertableJson::fromArray([]);

$this->expectException(AssertionFailedError::class);
$this->expectExceptionMessage('Cannot scope directly onto the first element of the root level because it is empty.');

$assert->first(function (AssertableJson $item) {
//
});
}

public function testFirstNestedScopeFailsWhenNoProps()
{
$assert = AssertableJson::fromArray([
'foo' => [],
]);

$this->expectException(AssertionFailedError::class);
$this->expectExceptionMessage('Cannot scope directly onto the first element of property [foo] because it is empty.');

$assert->has('foo', function (AssertableJson $assert) {
$assert->first(function (AssertableJson $item) {
//
});
});
}

public function testFirstScopeFailsWhenPropSingleValue()
{
$assert = AssertableJson::fromArray([
'foo' => 'bar',
]);

$this->expectException(AssertionFailedError::class);
$this->expectExceptionMessage('Property [foo] is not scopeable.');

$assert->first(function (AssertableJson $item) {
//
});
}

public function testFailsWhenNotInteractingWithAllPropsInScope()
{
$assert = AssertableJson::fromArray([
Expand Down
Loading

0 comments on commit a7ec58e

Please sign in to comment.