Skip to content
This repository was archived by the owner on Jun 16, 2021. It is now read-only.

Adds HigherOrderExpectations #12

Merged
merged 8 commits into from
Jun 10, 2021
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 2 additions & 3 deletions src/Concerns/Extendable.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,8 @@

namespace Pest\Expectations\Concerns;

use BadMethodCallException;
use Closure;
use Pest\Expectations\HigherOrderExpectation;

/**
* @internal
Expand Down Expand Up @@ -40,12 +40,11 @@ public static function hasExtend(string $name): bool
*
* @return mixed
*
* @throws BadMethodCallException
*/
public function __call(string $method, array $parameters)
{
if (!static::hasExtend($method)) {
throw new BadMethodCallException(sprintf('Method %s::%s does not exist.', static::class, $method));
return new HigherOrderExpectation($this, $method, true, ...$parameters);
}

/** @var Closure $extend */
Expand Down
42 changes: 27 additions & 15 deletions src/HigherOrderExpectation.php
Original file line number Diff line number Diff line change
Expand Up @@ -33,16 +33,24 @@ final class HigherOrderExpectation
/**
* @var string
*/
private $property;
private $name;

/**
* Creates a new higher order expectation.
*/
public function __construct(Expectation $original, string $property)
public function __construct(Expectation $original, string $name, bool $asMethod = false, ...$arguments)
Copy link
Member

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?

return (new HigherOrderExpectation($this, $method))->asMethod(...$parameters);

Copy link
Contributor Author

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:

private function expectation()
{
   return $this->expectation ??= // Set up the expectation here
}

Copy link
Contributor Author

@nedwors nedwors Jun 8, 2021

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:

return HigherOrderExpectation::forProperty($this, $name)

return HigherOrderExpectation::forMethod($this, $name, ...$parameters)

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.

Copy link
Member

@lukeraymonddowning lukeraymonddowning Jun 9, 2021

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.

Copy link
Contributor Author

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 🔥

{
$this->original = $original;
$this->property = $property;
$this->expectation = $this->expect($this->getPropertyValue());
$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());
}

/**
Expand All @@ -51,11 +59,11 @@ public function __construct(Expectation $original, string $property)
private function getPropertyValue()
{
if (is_array($this->original->value)) {
return $this->original->value[$this->property];
return $this->original->value[$this->name];
}

if (is_object($this->original->value)) {
return $this->original->value->{$this->property};
return $this->original->value->{$this->name};
}
}

Expand All @@ -76,6 +84,10 @@ public function not(): HigherOrderExpectation
*/
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);
}

Expand All @@ -88,13 +100,21 @@ public function __get(string $name): HigherOrderExpectation
return $this->not();
}

if (!$this->originalExpectationHasMethod($name)) {
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.
*/
Expand All @@ -108,12 +128,4 @@ private function performAssertion($name, ...$arguments)

return $this;
}

/**
* Determines if the original expectation has the given method name.
*/
private function originalExpectationHasMethod($name): bool
{
return method_exists($this->original, $name) || $this->original::hasExtend($name);
}
}
100 changes: 100 additions & 0 deletions tests/Expect/HigherOrder/methods.php
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,
],
];
}
}
49 changes: 49 additions & 0 deletions tests/Expect/HigherOrder/methodsAndProperties.php
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;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -48,11 +48,27 @@ function ($book) { $book->title->toEqual('Bar')->cost->toEqual(30); },
});

it('works with objects', function () {
expect((object) ['name' => 'foo', 'posts' => [['is_published' => true, 'title' => 'Foo'], ['is_published' => true, 'title' => 'Bar']]])
expect(new HasProperties)
->name->toEqual('foo')->not->toEqual('world')
->posts->toHaveCount(2)->each(function ($post) { $post->is_published->toBeTrue(); })
->posts->sequence(
function ($post) { $post->title->toEqual('Foo'); },
function ($post) { $post->title->toEqual('Bar'); },
);
});

class HasProperties
{
public $name = 'foo';

public $posts = [
[
'is_published' => true,
'title' => 'Foo'
],
[
'is_published' => true,
'title' => 'Bar'
]
];
}