This repository was archived by the owner on Jun 16, 2021. It is now read-only.
generated from pestphp/pest-plugin-template
-
-
Notifications
You must be signed in to change notification settings - Fork 7
Adds HigherOrderExpectations #12
Merged
Merged
Changes from 5 commits
Commits
Show all changes
8 commits
Select commit
Hold shift + click to select a range
59ea93e
Adds HigherOrderExpectations
nedwors b1baa80
Makes dynamic method handling more robust
nedwors fe617c9
Updates tests
nedwors 6e1e954
Updates doc block type
nedwors 9d8015c
Allows HigherOrderExpectations to be returned for methods
nedwors 09a13af
Refactor
nedwors d6f4819
Adds static constructors for clear instantiation
nedwors a56e8a4
Refactor for simpler instantiation
nedwors File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,131 @@ | ||
<?php | ||
|
||
declare(strict_types=1); | ||
|
||
namespace Pest\Expectations; | ||
|
||
use Pest\Expectations\Concerns\Expectations; | ||
|
||
/** | ||
* @internal | ||
* | ||
* @mixin Expectation | ||
*/ | ||
final class HigherOrderExpectation | ||
{ | ||
use Expectations; | ||
|
||
/** | ||
* @var Expectation | ||
*/ | ||
private $original; | ||
|
||
/** | ||
* @var Expectation|Each | ||
*/ | ||
private $expectation; | ||
|
||
/** | ||
* @var bool | ||
*/ | ||
private $opposite; | ||
|
||
/** | ||
* @var string | ||
*/ | ||
private $name; | ||
|
||
/** | ||
* Creates a new higher order expectation. | ||
*/ | ||
public function __construct(Expectation $original, string $name, bool $asMethod = false, ...$arguments) | ||
{ | ||
$this->original = $original; | ||
$this->name = $name; | ||
$this->expectation = $this->generateInitialExpectation($asMethod, ...$arguments); | ||
} | ||
|
||
/** | ||
* Generates the initial state of the expectation. | ||
*/ | ||
private function generateInitialExpectation($asMethod, ...$arguments) | ||
{ | ||
return $this->expect($asMethod ? $this->original->value->{$this->name}(...$arguments) : $this->getPropertyValue()); | ||
} | ||
|
||
/** | ||
* Retrieves the property value from the original expectation. | ||
*/ | ||
private function getPropertyValue() | ||
{ | ||
if (is_array($this->original->value)) { | ||
return $this->original->value[$this->name]; | ||
} | ||
|
||
if (is_object($this->original->value)) { | ||
return $this->original->value->{$this->name}; | ||
} | ||
} | ||
|
||
/** | ||
* Creates the opposite expectation for the value. | ||
*/ | ||
public function not(): HigherOrderExpectation | ||
{ | ||
$this->opposite = true; | ||
|
||
return $this; | ||
} | ||
|
||
/** | ||
* Dynamically calls methods on the class with the given arguments on each item. | ||
* | ||
* @param array<int|string, mixed> $arguments | ||
*/ | ||
public function __call(string $name, array $arguments): HigherOrderExpectation | ||
{ | ||
if (!$this->originalHasMethod($name)) { | ||
return new static($this->original, $name, true, ...$arguments); | ||
} | ||
|
||
return $this->performAssertion($name, ...$arguments); | ||
} | ||
|
||
/** | ||
* Accesses properties in the value or in the expectation. | ||
*/ | ||
public function __get(string $name): HigherOrderExpectation | ||
{ | ||
if ($name == 'not') { | ||
return $this->not(); | ||
} | ||
|
||
if (!$this->originalHasMethod($name)) { | ||
return new static($this->original, $name); | ||
} | ||
|
||
return $this->performAssertion($name); | ||
} | ||
|
||
/** | ||
* Determines if the original expectation has the given method name. | ||
*/ | ||
private function originalHasMethod($name): bool | ||
{ | ||
return method_exists($this->original, $name) || $this->original::hasExtend($name); | ||
} | ||
|
||
/** | ||
* Performs the given assertion with the current expectation. | ||
*/ | ||
private function performAssertion($name, ...$arguments) | ||
{ | ||
$this->expectation = $this->opposite | ||
? $this->expectation->not()->{$name}(...$arguments) | ||
: $this->expectation->{$name}(...$arguments); | ||
|
||
$this->opposite = false; | ||
|
||
return $this; | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,100 @@ | ||
<?php | ||
|
||
it('can access methods', function () { | ||
expect(new HasMethods) | ||
->name()->toBeString()->toEqual('Has Methods'); | ||
}); | ||
|
||
it('can access multiple methods', function () { | ||
expect(new HasMethods) | ||
->name()->toBeString()->toEqual('Has Methods') | ||
->quantity()->toBeInt()->toEqual(20); | ||
}); | ||
|
||
it('works with not', function () { | ||
expect(new HasMethods) | ||
->name()->not->toEqual('world')->toEqual('Has Methods') | ||
->quantity()->toEqual(20)->not()->toEqual('bar')->not->toBeNull; | ||
}); | ||
|
||
it('can accept arguments', function () { | ||
expect(new HasMethods) | ||
->multiply(5, 4)->toBeInt->toEqual(20); | ||
}); | ||
|
||
it('works with each', function () { | ||
expect(new HasMethods) | ||
->attributes()->toBeArray->each->not()->toBeNull | ||
->attributes()->each(function ($attribute) { | ||
$attribute->not->toBeNull(); | ||
}); | ||
}); | ||
|
||
it('works inside of each', function () { | ||
expect(new HasMethods) | ||
->books()->each(function ($book) { | ||
$book->title->not->toBeNull->cost->toBeGreaterThan(19); | ||
}); | ||
}); | ||
|
||
it('works with sequence', function () { | ||
expect(new HasMethods) | ||
->books()->sequence( | ||
function ($book) { $book->title->toEqual('Foo')->cost->toEqual(20); }, | ||
function ($book) { $book->title->toEqual('Bar')->cost->toEqual(30); }, | ||
); | ||
}); | ||
|
||
it('can compose complex expectations', function () { | ||
expect(new HasMethods) | ||
->toBeObject() | ||
->name()->toEqual('Has Methods')->not()->toEqual('bar') | ||
->quantity()->not->toEqual('world')->toEqual(20)->toBeInt | ||
->multiply(3, 4)->not->toBeString->toEqual(12) | ||
->attributes()->toBeArray() | ||
->books()->toBeArray->each->not->toBeEmpty | ||
->books()->sequence( | ||
function ($book) { $book->title->toEqual('Foo')->cost->toEqual(20); }, | ||
function ($book) { $book->title->toEqual('Bar')->cost->toEqual(30); }, | ||
); | ||
}); | ||
|
||
class HasMethods | ||
{ | ||
public function name() | ||
{ | ||
return 'Has Methods'; | ||
} | ||
|
||
public function quantity() | ||
{ | ||
return 20; | ||
} | ||
|
||
public function multiply($x, $y) | ||
{ | ||
return $x * $y; | ||
} | ||
|
||
public function attributes() | ||
{ | ||
return [ | ||
'name' => $this->name(), | ||
'quantity' => $this->quantity() | ||
]; | ||
} | ||
|
||
public function books() | ||
{ | ||
return [ | ||
[ | ||
'title' => 'Foo', | ||
'cost' => 20, | ||
], | ||
[ | ||
'title' => 'Bar', | ||
'cost' => 30, | ||
], | ||
]; | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,49 @@ | ||
<?php | ||
|
||
it('can access methods and properties', function () { | ||
expect(new HasMethodsAndProperties) | ||
->name->toEqual('Has Methods and Properties')->not()->toEqual('bar') | ||
->multiply(3, 4)->not->toBeString->toEqual(12) | ||
->posts->each(fn ($post) => $post->is_published->toBeTrue) | ||
->books()->toBeArray() | ||
->posts->toBeArray->each->not->toBeEmpty | ||
->books()->sequence( | ||
function ($book) { $book->title->toEqual('Foo')->cost->toEqual(20); }, | ||
function ($book) { $book->title->toEqual('Bar')->cost->toEqual(30); }, | ||
); | ||
}); | ||
|
||
class HasMethodsAndProperties | ||
{ | ||
public $name = 'Has Methods and Properties'; | ||
|
||
public $posts = [ | ||
[ | ||
'is_published' => true, | ||
'title' => 'Foo' | ||
], | ||
[ | ||
'is_published' => true, | ||
'title' => 'Bar' | ||
] | ||
]; | ||
|
||
public function books() | ||
{ | ||
return [ | ||
[ | ||
'title' => 'Foo', | ||
'cost' => 20, | ||
], | ||
[ | ||
'title' => 'Bar', | ||
'cost' => 30, | ||
], | ||
]; | ||
} | ||
|
||
public function multiply($x, $y) | ||
{ | ||
return $x * $y; | ||
} | ||
} |
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Could the
$asMethod
and$arguments
be passed in a separate method?There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I thought exactly this but went with parameters to allow the expectation set-up to happen in the constructor?
I'm not sure of a clean way to defer the setup of the expectation in the class without having everywhere that calls
$this->expectation
to instead call$this->expectation()
and have that method as:There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@lukeraymonddowning - I've added static constructors as one way at solving this:
I'm not sure if static constructors are in fitting with this project's code style but I thought it was at least worth presenting it.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Could we instead assume
asMethod
if the constructor received more than 2 arguments? Perhaps remove the varadic and have it as an array.Just thinking that when you look at the static constructors they're actually remarkably similar and properties will never have arguments.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@lukeraymonddowning chef's kiss. Great call, thank you 🔥