mirror of
https://github.com/pestphp/pest.git
synced 2026-03-07 00:07:22 +01:00
Compare commits
9 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 9288490613 | |||
| dc9f70cd5c | |||
| 7a0bf9db97 | |||
| 4f89a2ae16 | |||
| 28d8822de0 | |||
| af3ca742c2 | |||
| 0bf3471968 | |||
| 6fc55becc8 | |||
| 2d85842777 |
@ -6,6 +6,13 @@ and this project adheres to [Semantic Versioning](http://semver.org/).
|
|||||||
|
|
||||||
## [Unreleased]
|
## [Unreleased]
|
||||||
|
|
||||||
|
## [v0.1.1 (2020-05-14)](https://github.com/pestphp/pest/compare/v0.1.0...v0.1.1)
|
||||||
|
### Added
|
||||||
|
- `test` function without any arguments returns the current test case ([6fc55be](https://github.com/pestphp/pest/commit/6fc55becc8aecff685a958617015be1a4c118b01))
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
- "No coverage driver error" now returns proper error on Laravel ([28d8822](https://github.com/pestphp/pest/commit/28d8822de01f4fa92c62d8b8e019313f382b97e9))
|
||||||
|
|
||||||
## [v0.1.0 (2020-05-09)](https://github.com/pestphp/pest/commit/de2929077b344a099ef9c2ddc2f48abce14e248f)
|
## [v0.1.0 (2020-05-09)](https://github.com/pestphp/pest/commit/de2929077b344a099ef9c2ddc2f48abce14e248f)
|
||||||
### Added
|
### Added
|
||||||
- First version
|
- First version
|
||||||
|
|||||||
@ -92,6 +92,8 @@ trait TestCase
|
|||||||
*/
|
*/
|
||||||
protected function setUp(): void
|
protected function setUp(): void
|
||||||
{
|
{
|
||||||
|
TestSuite::getInstance()->test = $this;
|
||||||
|
|
||||||
parent::setUp();
|
parent::setUp();
|
||||||
|
|
||||||
$beforeEach = TestSuite::getInstance()->beforeEach->get(self::$__filename);
|
$beforeEach = TestSuite::getInstance()->beforeEach->get(self::$__filename);
|
||||||
@ -109,6 +111,8 @@ trait TestCase
|
|||||||
$this->__callClosure($afterEach, func_get_args());
|
$this->__callClosure($afterEach, func_get_args());
|
||||||
|
|
||||||
parent::tearDown();
|
parent::tearDown();
|
||||||
|
|
||||||
|
TestSuite::getInstance()->test = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@ -9,7 +9,6 @@ use Pest\Actions\AddsDefaults;
|
|||||||
use Pest\Actions\AddsTests;
|
use Pest\Actions\AddsTests;
|
||||||
use Pest\Actions\LoadStructure;
|
use Pest\Actions\LoadStructure;
|
||||||
use Pest\Actions\ValidatesConfiguration;
|
use Pest\Actions\ValidatesConfiguration;
|
||||||
use Pest\Exceptions\CodeCoverageDriverNotAvailable;
|
|
||||||
use Pest\TestSuite;
|
use Pest\TestSuite;
|
||||||
use PHPUnit\Framework\TestSuite as BaseTestSuite;
|
use PHPUnit\Framework\TestSuite as BaseTestSuite;
|
||||||
use PHPUnit\TextUI\Command as BaseCommand;
|
use PHPUnit\TextUI\Command as BaseCommand;
|
||||||
@ -122,7 +121,10 @@ final class Command extends BaseCommand
|
|||||||
|
|
||||||
if ($result === 0 && $this->testSuite->coverage) {
|
if ($result === 0 && $this->testSuite->coverage) {
|
||||||
if (!Coverage::isAvailable()) {
|
if (!Coverage::isAvailable()) {
|
||||||
throw new CodeCoverageDriverNotAvailable();
|
$this->output->writeln(
|
||||||
|
"\n <fg=white;bg=red;options=bold> ERROR </> No code coverage driver is available.</>",
|
||||||
|
);
|
||||||
|
exit(1);
|
||||||
}
|
}
|
||||||
|
|
||||||
$coverage = Coverage::report($this->output);
|
$coverage = Coverage::report($this->output);
|
||||||
|
|||||||
@ -1,24 +0,0 @@
|
|||||||
<?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 CodeCoverageDriverNotAvailable extends InvalidArgumentException implements ExceptionInterface, RenderlessEditor, RenderlessTrace
|
|
||||||
{
|
|
||||||
/**
|
|
||||||
* Creates a new instance of test already exist.
|
|
||||||
*/
|
|
||||||
public function __construct()
|
|
||||||
{
|
|
||||||
parent::__construct('No code coverage driver is available');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -4,11 +4,16 @@ declare(strict_types=1);
|
|||||||
|
|
||||||
namespace Pest\Support;
|
namespace Pest\Support;
|
||||||
|
|
||||||
|
use ReflectionClass;
|
||||||
|
use Throwable;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @internal
|
* @internal
|
||||||
*/
|
*/
|
||||||
final class HigherOrderMessage
|
final class HigherOrderMessage
|
||||||
{
|
{
|
||||||
|
public const UNDEFINED_METHOD = 'Method %s does not exist';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The filename where the function was originally called.
|
* The filename where the function was originally called.
|
||||||
*
|
*
|
||||||
@ -57,4 +62,27 @@ final class HigherOrderMessage
|
|||||||
$this->methodName = $methodName;
|
$this->methodName = $methodName;
|
||||||
$this->arguments = $arguments;
|
$this->arguments = $arguments;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Re-throws the given `$throwable` with the good line and filename.
|
||||||
|
*
|
||||||
|
* @return mixed
|
||||||
|
*/
|
||||||
|
public function call(object $target)
|
||||||
|
{
|
||||||
|
try {
|
||||||
|
return Reflection::call($target, $this->methodName, $this->arguments);
|
||||||
|
} catch (Throwable $throwable) {
|
||||||
|
Reflection::setPropertyValue($throwable, 'file', $this->filename);
|
||||||
|
Reflection::setPropertyValue($throwable, 'line', $this->line);
|
||||||
|
|
||||||
|
if ($throwable->getMessage() === sprintf(self::UNDEFINED_METHOD, $this->methodName)) {
|
||||||
|
/** @var \ReflectionClass $reflection */
|
||||||
|
$reflection = (new ReflectionClass($target))->getParentClass();
|
||||||
|
Reflection::setPropertyValue($throwable, 'message', sprintf('Call to undefined method %s::%s()', $reflection->getName(), $this->methodName));
|
||||||
|
}
|
||||||
|
|
||||||
|
throw $throwable;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -4,16 +4,11 @@ declare(strict_types=1);
|
|||||||
|
|
||||||
namespace Pest\Support;
|
namespace Pest\Support;
|
||||||
|
|
||||||
use ReflectionClass;
|
|
||||||
use Throwable;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @internal
|
* @internal
|
||||||
*/
|
*/
|
||||||
final class HigherOrderMessageCollection
|
final class HigherOrderMessageCollection
|
||||||
{
|
{
|
||||||
public const UNDEFINED_METHOD = 'Method %s does not exist';
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @var array<int, HigherOrderMessage>
|
* @var array<int, HigherOrderMessage>
|
||||||
*/
|
*/
|
||||||
@ -35,7 +30,7 @@ final class HigherOrderMessageCollection
|
|||||||
public function chain(object $target): void
|
public function chain(object $target): void
|
||||||
{
|
{
|
||||||
foreach ($this->messages as $message) {
|
foreach ($this->messages as $message) {
|
||||||
$target = $this->attempt($target, $message);
|
$target = $message->call($target);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -45,30 +40,7 @@ final class HigherOrderMessageCollection
|
|||||||
public function proxy(object $target): void
|
public function proxy(object $target): void
|
||||||
{
|
{
|
||||||
foreach ($this->messages as $message) {
|
foreach ($this->messages as $message) {
|
||||||
$this->attempt($target, $message);
|
$message->call($target);
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Re-throws the given `$throwable` with the good line and filename.
|
|
||||||
*
|
|
||||||
* @return mixed
|
|
||||||
*/
|
|
||||||
private function attempt(object $target, HigherOrderMessage $message)
|
|
||||||
{
|
|
||||||
try {
|
|
||||||
return Reflection::call($target, $message->methodName, $message->arguments);
|
|
||||||
} catch (Throwable $throwable) {
|
|
||||||
Reflection::setPropertyValue($throwable, 'file', $message->filename);
|
|
||||||
Reflection::setPropertyValue($throwable, 'line', $message->line);
|
|
||||||
|
|
||||||
if ($throwable->getMessage() === sprintf(self::UNDEFINED_METHOD, $message->methodName)) {
|
|
||||||
/** @var \ReflectionClass $reflection */
|
|
||||||
$reflection = (new ReflectionClass($target))->getParentClass();
|
|
||||||
Reflection::setPropertyValue($throwable, 'message', sprintf('Call to undefined method %s::%s()', $reflection->getName(), $message->methodName));
|
|
||||||
}
|
|
||||||
|
|
||||||
throw $throwable;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
83
src/Support/HigherOrderTapProxy.php
Normal file
83
src/Support/HigherOrderTapProxy.php
Normal file
@ -0,0 +1,83 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace Pest\Support;
|
||||||
|
|
||||||
|
use ReflectionClass;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @internal
|
||||||
|
*/
|
||||||
|
final class HigherOrderTapProxy
|
||||||
|
{
|
||||||
|
private const UNDEFINED_PROPERTY = 'Undefined property: P\\';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The target being tapped.
|
||||||
|
*
|
||||||
|
* @var mixed
|
||||||
|
*/
|
||||||
|
public $target;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a new tap proxy instance.
|
||||||
|
*
|
||||||
|
* @param mixed $target
|
||||||
|
*/
|
||||||
|
public function __construct($target)
|
||||||
|
{
|
||||||
|
$this->target = $target;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Dynamically sets properties on the target.
|
||||||
|
*
|
||||||
|
* @param mixed $value
|
||||||
|
*/
|
||||||
|
public function __set(string $property, $value): void
|
||||||
|
{
|
||||||
|
// @phpstan-ignore-next-line
|
||||||
|
$this->target->{$property} = $value;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Dynamically pass properties gets to the target.
|
||||||
|
*
|
||||||
|
* @return mixed
|
||||||
|
*/
|
||||||
|
public function __get(string $property)
|
||||||
|
{
|
||||||
|
try {
|
||||||
|
// @phpstan-ignore-next-line
|
||||||
|
return $this->target->{$property};
|
||||||
|
} catch (\Throwable $throwable) {
|
||||||
|
Reflection::setPropertyValue($throwable, 'file', Backtrace::file());
|
||||||
|
Reflection::setPropertyValue($throwable, 'line', Backtrace::line());
|
||||||
|
|
||||||
|
if (Str::startsWith($message = $throwable->getMessage(), self::UNDEFINED_PROPERTY)) {
|
||||||
|
/** @var \ReflectionClass $reflection */
|
||||||
|
$reflection = (new ReflectionClass($this->target))->getParentClass();
|
||||||
|
Reflection::setPropertyValue($throwable, 'message', sprintf('Undefined property %s::$%s', $reflection->getName(), $property));
|
||||||
|
}
|
||||||
|
|
||||||
|
throw $throwable;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Dynamically pass method calls to the target.
|
||||||
|
*
|
||||||
|
* @param array<int, mixed> $arguments
|
||||||
|
*
|
||||||
|
* @return mixed
|
||||||
|
*/
|
||||||
|
public function __call(string $methodName, array $arguments)
|
||||||
|
{
|
||||||
|
$filename = Backtrace::file();
|
||||||
|
$line = Backtrace::line();
|
||||||
|
|
||||||
|
return (new HigherOrderMessage($filename, $line, $methodName, $arguments))
|
||||||
|
->call($this->target);
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -16,6 +16,13 @@ use Pest\Repositories\TestRepository;
|
|||||||
*/
|
*/
|
||||||
final class TestSuite
|
final class TestSuite
|
||||||
{
|
{
|
||||||
|
/**
|
||||||
|
* Holds the current test case.
|
||||||
|
*
|
||||||
|
* @var \PHPUnit\Framework\TestCase|null
|
||||||
|
*/
|
||||||
|
public $test;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Holds the tests repository.
|
* Holds the tests repository.
|
||||||
*
|
*
|
||||||
|
|||||||
@ -8,6 +8,7 @@ use Pest\PendingObjects\BeforeEachCall;
|
|||||||
use Pest\PendingObjects\TestCall;
|
use Pest\PendingObjects\TestCall;
|
||||||
use Pest\PendingObjects\UsesCall;
|
use Pest\PendingObjects\UsesCall;
|
||||||
use Pest\Support\Backtrace;
|
use Pest\Support\Backtrace;
|
||||||
|
use Pest\Support\HigherOrderTapProxy;
|
||||||
use Pest\TestSuite;
|
use Pest\TestSuite;
|
||||||
use PHPUnit\Framework\TestCase;
|
use PHPUnit\Framework\TestCase;
|
||||||
|
|
||||||
@ -59,8 +60,12 @@ function uses(string ...$classAndTraits): UsesCall
|
|||||||
*
|
*
|
||||||
* @return TestCall|TestCase|mixed
|
* @return TestCall|TestCase|mixed
|
||||||
*/
|
*/
|
||||||
function test(string $description, Closure $closure = null): TestCall
|
function test(string $description = null, Closure $closure = null)
|
||||||
{
|
{
|
||||||
|
if ($description === null && TestSuite::getInstance()->test) {
|
||||||
|
return new HigherOrderTapProxy(TestSuite::getInstance()->test);
|
||||||
|
}
|
||||||
|
|
||||||
$filename = Backtrace::file();
|
$filename = Backtrace::file();
|
||||||
|
|
||||||
return new TestCall(TestSuite::getInstance(), $filename, $description, $closure);
|
return new TestCall(TestSuite::getInstance(), $filename, $description, $closure);
|
||||||
|
|||||||
@ -47,6 +47,12 @@
|
|||||||
✓ it catch exceptions
|
✓ it catch exceptions
|
||||||
✓ it catch exceptions and messages
|
✓ it catch exceptions and messages
|
||||||
|
|
||||||
|
PASS Tests\Features\Helpers
|
||||||
|
✓ it can set/get properties on $this
|
||||||
|
✓ it throws error if property do not exist
|
||||||
|
✓ it allows to call underlying protected/private methods
|
||||||
|
✓ it throws error if method do not exist
|
||||||
|
|
||||||
PASS Tests\Features\HigherOrderMessages
|
PASS Tests\Features\HigherOrderMessages
|
||||||
✓ it proxies calls to object
|
✓ it proxies calls to object
|
||||||
|
|
||||||
@ -126,5 +132,5 @@
|
|||||||
WARN Tests\Visual\Success
|
WARN Tests\Visual\Success
|
||||||
s visual snapshot of test suite on success
|
s visual snapshot of test suite on success
|
||||||
|
|
||||||
Tests: 6 skipped, 65 passed
|
Tests: 6 skipped, 69 passed
|
||||||
Time: 2.50s
|
Time: 2.63s
|
||||||
|
|||||||
43
tests/Features/Helpers.php
Normal file
43
tests/Features/Helpers.php
Normal file
@ -0,0 +1,43 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
function addUser()
|
||||||
|
{
|
||||||
|
test()->user = 'nuno';
|
||||||
|
}
|
||||||
|
|
||||||
|
it('can set/get properties on $this', function () {
|
||||||
|
addUser();
|
||||||
|
assertEquals('nuno', $this->user);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('throws error if property do not exist', function () {
|
||||||
|
test()->user;
|
||||||
|
})->throws(\Whoops\Exception\ErrorException::class, 'Undefined property PHPUnit\Framework\TestCase::$user');
|
||||||
|
|
||||||
|
class User
|
||||||
|
{
|
||||||
|
public function getName()
|
||||||
|
{
|
||||||
|
return 'nuno';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function mockUser()
|
||||||
|
{
|
||||||
|
$mock = test()->createMock(User::class);
|
||||||
|
|
||||||
|
$mock->method('getName')
|
||||||
|
->willReturn('maduro');
|
||||||
|
|
||||||
|
return $mock;
|
||||||
|
}
|
||||||
|
|
||||||
|
it('allows to call underlying protected/private methods', function () {
|
||||||
|
$user = mockUser();
|
||||||
|
|
||||||
|
assertEquals('maduro', $user->getName());
|
||||||
|
});
|
||||||
|
|
||||||
|
it('throws error if method do not exist', function () {
|
||||||
|
test()->name();
|
||||||
|
})->throws(\ReflectionException::class, 'Call to undefined method PHPUnit\Framework\TestCase::name()');
|
||||||
Reference in New Issue
Block a user