Merge pull request #289 from pestphp/feat/mock

feat: adds built-in mocking
This commit is contained in:
Nuno Maduro
2021-05-15 01:03:07 +01:00
committed by GitHub
7 changed files with 146 additions and 19 deletions

View File

@ -0,0 +1,24 @@
<?php
declare(strict_types=1);
namespace Pest\Exceptions;
use InvalidArgumentException;
use NunoMaduro\Collision\Contracts\RenderlessEditor;
use NunoMaduro\Collision\Contracts\RenderlessTrace;
use Symfony\Component\Console\Exception\ExceptionInterface;
/**
* @internal
*/
final class MissingDependency extends InvalidArgumentException implements ExceptionInterface, RenderlessEditor, RenderlessTrace
{
/**
* Creates a new instance of missing dependency.
*/
public function __construct(string $feature, string $dependency)
{
parent::__construct(sprintf('The feature "%s" requires "%s".', $feature, $dependency));
}
}

View File

@ -3,6 +3,7 @@
declare(strict_types=1); declare(strict_types=1);
use Pest\Datasets; use Pest\Datasets;
use Pest\Mock;
use Pest\PendingObjects\AfterEachCall; use Pest\PendingObjects\AfterEachCall;
use Pest\PendingObjects\BeforeEachCall; use Pest\PendingObjects\BeforeEachCall;
use Pest\PendingObjects\TestCall; use Pest\PendingObjects\TestCall;
@ -104,3 +105,15 @@ function afterAll(Closure $closure): void
{ {
TestSuite::getInstance()->afterAll->set($closure); TestSuite::getInstance()->afterAll->set($closure);
} }
if (!function_exists('mock')) {
/**
* Creates a new mock with the given class or object.
*
* @param string|object $object
*/
function mock($object): Mock
{
return new Mock($object);
}
}

76
src/Mock.php Normal file
View File

@ -0,0 +1,76 @@
<?php
declare(strict_types=1);
namespace Pest;
use InvalidArgumentException;
use Mockery;
use Pest\Exceptions\MissingDependency;
/**
* @mixin \Mockery\MockInterface
*/
final class Mock
{
/**
* The object being mocked.
*
* @readonly
*
* @var \Mockery\MockInterface|\Mockery\LegacyMockInterface
*/
private $mock;
/**
* Creates a new mock instance.
*
* @param string|object $object
*/
public function __construct($object)
{
if (!class_exists(Mockery::class)) {
throw new MissingDependency('Mocking', 'mockery/mockery');
}
$this->mock = Mockery::mock($object);
}
/**
* Define mock expectations.
*
* @param mixed ...$methods
*
* @return \Mockery\MockInterface|\Mockery\LegacyMockInterface
*/
public function expect(...$methods)
{
foreach ($methods as $method => $expectation) {
/* @phpstan-ignore-next-line */
$method = $this->mock
->shouldReceive((string) $method)
->once();
if (!is_callable($expectation)) {
throw new InvalidArgumentException(sprintf('Method %s from %s expects a callable as expectation.', $method, $method->mock()->mockery_getName(), ));
}
$method->andReturnUsing($expectation);
}
return $this->mock;
}
/**
* Proxies calls to the original mock object.
*
* @param array<int, mixed> $arguments
*
* @return mixed
*/
public function __call(string $method, array $arguments)
{
/* @phpstan-ignore-next-line */
return $this->mock->{$method}($arguments);
}
}

View File

@ -63,6 +63,7 @@
✓ it allows to call underlying protected/private methods ✓ it allows to call underlying protected/private methods
✓ it throws error if method do not exist ✓ it throws error if method do not exist
✓ it can forward unexpected calls to any global function ✓ it can forward unexpected calls to any global function
✓ it can use helpers from helpers file
PASS Tests\Features\HigherOrderTests PASS Tests\Features\HigherOrderTests
✓ it proxies calls to object ✓ it proxies calls to object
@ -77,7 +78,8 @@
✓ it will throw exception from call if no macro exists ✓ it will throw exception from call if no macro exists
PASS Tests\Features\Mocks PASS Tests\Features\Mocks
✓ it has bar ✓ it can mock methods
✓ it allows access to the underlying mockery mock
PASS Tests\Features\PendingHigherOrderTests PASS Tests\Features\PendingHigherOrderTests
✓ get 'foo' → get 'bar' → expect true → toBeTrue ✓ get 'foo' → get 'bar' → expect true → toBeTrue
@ -221,5 +223,5 @@
✓ it is a test ✓ it is a test
✓ it uses correct parent class ✓ it uses correct parent class
Tests: 7 skipped, 119 passed Tests: 7 skipped, 121 passed

View File

@ -42,3 +42,5 @@ it('throws error if method do not exist', function () {
})->throws(\ReflectionException::class, 'Call to undefined method PHPUnit\Framework\TestCase::name()'); })->throws(\ReflectionException::class, 'Call to undefined method PHPUnit\Framework\TestCase::name()');
it('can forward unexpected calls to any global function')->_assertThat(); it('can forward unexpected calls to any global function')->_assertThat();
it('can use helpers from helpers file')->myAssertTrue(true);

View File

@ -1,17 +1,32 @@
<?php <?php
use function Tests\mock; use Mockery\CompositeExpectation;
use Mockery\MockInterface;
interface Foo interface Http
{ {
public function bar(): int; public function get(): string;
} }
it('has bar', function () { it('can mock methods', function () {
$mock = mock(Foo::class); $mock = mock(Http::class)->expect(
$mock->shouldReceive('bar') get: fn () => 'foo',
->times(1) );
->andReturn(2);
$mock->bar(); expect($mock->get())->toBe('foo');
}); })->skip(((float) phpversion()) < 8.0);
it('can access to arguments', function () {
$mock = mock(Http::class)->expect(
get: fn ($foo) => $foo,
);
expect($mock->get('foo'))->toBe('foo');
})->skip(((float) phpversion()) < 8.0);
it('allows access to the underlying mockery mock', function () {
$mock = mock(Http::class);
expect($mock->expect())->toBeInstanceOf(MockInterface::class);
expect($mock->shouldReceive())->toBeInstanceOf(CompositeExpectation::class);
})->skip(((float) phpversion()) < 8.0);

View File

@ -1,11 +1,6 @@
<?php <?php
namespace Tests; function myAssertTrue($value)
use Mockery;
use Mockery\MockInterface;
function mock(string $class): MockInterface
{ {
return Mockery::mock($class); test()->assertTrue($value);
} }