Compare commits

...

22 Commits

Author SHA1 Message Date
69f6a22121 docs: updates changelog 2020-05-21 22:22:34 +02:00
9a179d2891 chore: coding style 2020-05-21 21:48:47 +02:00
6c4be0190e feat: adds plugin uses api 2020-05-21 21:44:05 +02:00
5c6bb43d8d docs: updates readme badges 2020-05-20 01:30:05 +02:00
e536c28e34 docs: updates readme 2020-05-16 22:58:45 +02:00
67bb23cda3 Update README.md 2020-05-16 22:57:48 +02:00
59398cfcf8 docs: updates license 2020-05-16 22:37:50 +02:00
182377abe3 docs: adds twitter link 2020-05-16 22:34:00 +02:00
242e74964b docs: updates readme banner 2020-05-16 22:24:20 +02:00
9a2c7e45f7 docs: updates discord link 2020-05-15 18:19:56 +02:00
99205c4aa8 docs: updates changelog 2020-05-15 02:15:32 +02:00
1268384fb3 Merge pull request #7 from pestphp/feat/helpers
feat(helpers): adds helpers file
2020-05-15 02:12:12 +02:00
c97fd17120 feat(helpers): adds helpers file 2020-05-14 23:03:56 +02:00
9288490613 docs: updates changelog 2020-05-14 22:23:34 +02:00
dc9f70cd5c docs: updates changelog 2020-05-14 22:22:53 +02:00
7a0bf9db97 chore: reverts return type on globals 2020-05-14 22:18:13 +02:00
4f89a2ae16 chore: cs 2020-05-14 22:15:43 +02:00
28d8822de0 fix: error "No code coverage driver is available" in laravel 2020-05-14 22:05:08 +02:00
af3ca742c2 feat: improves ide support in laravel 2020-05-14 00:52:04 +02:00
0bf3471968 tests: fixes failing test 2020-05-13 22:55:38 +02:00
6fc55becc8 feat: adds support for global helpers 2020-05-13 22:51:50 +02:00
2d85842777 refacto: higher order messages 2020-05-13 22:21:59 +02:00
21 changed files with 300 additions and 73 deletions

View File

@ -6,6 +6,21 @@ and this project adheres to [Semantic Versioning](http://semver.org/).
## [Unreleased]
## [v0.1.3 (2020-05-21)](https://github.com/pestphp/pest/compare/v0.1.2...v0.1.3)
### Added
- `Plugin::uses()` method for making traits globally available ([6c4be01](https://github.com/pestphp/pest/commit/6c4be0190e9493702a976b996bbbf5150cc6bb53))
## [v0.1.2 (2020-05-15)](https://github.com/pestphp/pest/compare/v0.1.1...v0.1.2)
### Added
- Support to custom helpers ([#7](https://github.com/pestphp/pest/pull/7))
## [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)
### Added
- First version

View File

@ -1,10 +1,10 @@
<p align="center">
<img src="https://next.pestphp.com/assets/img/og.png" width="600" alt="PEST Preview">
<img src="https://raw.githubusercontent.com/pestphp/art/master/readme.png" width="600" alt="PEST">
<p align="center">
<a href="https://github.com/pestphp/pest/actions"><img src="https://github.com/pest/pestphp/workflows/tests/badge.svg" alt="Build Status"></a>
<a href="https://packagist.org/packages/pestphp/pest"><img src="https://poser.pugx.org/pestphp/pest/d/total.svg" alt="Total Downloads"></a>
<a href="https://packagist.org/packages/pestphp/pest"><img src="https://poser.pugx.org/pestphp/pest/v/stable.svg" alt="Latest Version"></a>
<a href="https://packagist.org/packages/pestphp/pest"><img src="https://poser.pugx.org/pestphp/pest/license.svg" alt="License"></a>
<a href="https://github.com/pestphp/pest/actions"><img alt="GitHub Workflow Status (master)" src="https://img.shields.io/github/workflow/status/pestphp/pest/Continuous Integration/master"></a>
<a href="https://packagist.org/packages/pestphp/pest"><img alt="Total Downloads" src="https://img.shields.io/packagist/dt/pestphp/pest"></a>
<a href="https://packagist.org/packages/pestphp/pest"><img alt="Latest Version" src="https://img.shields.io/packagist/v/pestphp/pest"></a>
<a href="https://packagist.org/packages/pestphp/pest"><img alt="License" src="https://img.shields.io/packagist/l/pestphp/pest"></a>
</p>
</p>
@ -12,6 +12,7 @@
**Pest** it's an elegant PHP Testing Framework with a focus on simplicity. It was carefully crafted to bring the joy of testing to PHP.
- Explore the docs: **[pestphp.com »](https://pestphp.com)**
- Join the Discord Server: **[discord.gg/4UMHUb5 »](https://discord.gg/4UMHUb5)**
- Follow us on Twitter: **[@pestphp »](https://twitter.com/pestphp)**
- Join us on the Discord Server: **[discord.gg/bMAJv82 »](https://discord.gg/bMAJv82)**
Pest was created by **[Nuno Maduro](https://twitter.com/enunomaduro)** and is open-sourced software licensed under the **[MIT license](https://opensource.org/licenses/MIT)**.
Pest was created by **[Nuno Maduro](https://twitter.com/enunomaduro)** under the **[Sponsorware license](https://github.com/sponsorware/docs)**. It got open-sourced and is now licensed under the **[MIT license](https://opensource.org/licenses/MIT)**.

View File

@ -34,7 +34,10 @@
"autoload-dev": {
"psr-4": {
"Tests\\": "tests/PHPUnit/"
}
},
"files": [
"tests/Autoload.php"
]
},
"require-dev": {
"ergebnis/phpstan-rules": "^0.14.4",

View File

@ -22,6 +22,7 @@ final class LoadStructure
*/
private const STRUCTURE = [
'Datasets.php',
'Helpers.php',
'Pest.php',
'Datasets',
];

View File

@ -92,6 +92,8 @@ trait TestCase
*/
protected function setUp(): void
{
TestSuite::getInstance()->test = $this;
parent::setUp();
$beforeEach = TestSuite::getInstance()->beforeEach->get(self::$__filename);
@ -109,6 +111,8 @@ trait TestCase
$this->__callClosure($afterEach, func_get_args());
parent::tearDown();
TestSuite::getInstance()->test = null;
}
/**

View File

@ -9,7 +9,6 @@ use Pest\Actions\AddsDefaults;
use Pest\Actions\AddsTests;
use Pest\Actions\LoadStructure;
use Pest\Actions\ValidatesConfiguration;
use Pest\Exceptions\CodeCoverageDriverNotAvailable;
use Pest\TestSuite;
use PHPUnit\Framework\TestSuite as BaseTestSuite;
use PHPUnit\TextUI\Command as BaseCommand;
@ -122,7 +121,10 @@ final class Command extends BaseCommand
if ($result === 0 && $this->testSuite->coverage) {
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);

View File

@ -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');
}
}

View File

@ -33,18 +33,29 @@ final class PestInstallCommand extends Command
public function handle(): void
{
/* @phpstan-ignore-next-line */
$target = base_path('tests/Pest.php');
$pest = base_path('tests/Pest.php');
/* @phpstan-ignore-next-line */
$helpers = base_path('tests/Helpers.php');
if (File::exists($target)) {
throw new InvalidConsoleArgument(sprintf('%s already exist', $target));
foreach ([$pest, $helpers] as $file) {
if (File::exists($file)) {
throw new InvalidConsoleArgument(sprintf('%s already exist', $file));
}
}
File::copy(implode(DIRECTORY_SEPARATOR, [
dirname(__DIR__, 3),
'stubs',
'Pest.php',
]), $target);
]), $pest);
File::copy(implode(DIRECTORY_SEPARATOR, [
dirname(__DIR__, 3),
'stubs',
'Helpers.php',
]), $helpers);
$this->output->success('`tests/Pest.php` created successfully.');
$this->output->success('`tests/Helpers.php` created successfully.');
}
}

28
src/Plugin.php Normal file
View File

@ -0,0 +1,28 @@
<?php
declare(strict_types=1);
namespace Pest;
final class Plugin
{
/**
* The lazy callables to be executed
* once the test suite boots.
*
* @var array<int, callable>
*
* @internal
*/
public static $callables = [];
/**
* Lazy loads an `uses` call on the context of plugins.
*/
public static function uses(string ...$traits): void
{
self::$callables[] = function () use ($traits): void {
uses(...$traits)->in(TestSuite::getInstance()->rootPath . DIRECTORY_SEPARATOR . 'tests');
};
}
}

View File

@ -4,11 +4,16 @@ declare(strict_types=1);
namespace Pest\Support;
use ReflectionClass;
use Throwable;
/**
* @internal
*/
final class HigherOrderMessage
{
public const UNDEFINED_METHOD = 'Method %s does not exist';
/**
* The filename where the function was originally called.
*
@ -57,4 +62,27 @@ final class HigherOrderMessage
$this->methodName = $methodName;
$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;
}
}
}

View File

@ -4,16 +4,11 @@ declare(strict_types=1);
namespace Pest\Support;
use ReflectionClass;
use Throwable;
/**
* @internal
*/
final class HigherOrderMessageCollection
{
public const UNDEFINED_METHOD = 'Method %s does not exist';
/**
* @var array<int, HigherOrderMessage>
*/
@ -35,7 +30,7 @@ final class HigherOrderMessageCollection
public function chain(object $target): void
{
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
{
foreach ($this->messages as $message) {
$this->attempt($target, $message);
}
}
/**
* 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;
$message->call($target);
}
}
}

View 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);
}
}

View File

@ -16,6 +16,13 @@ use Pest\Repositories\TestRepository;
*/
final class TestSuite
{
/**
* Holds the current test case.
*
* @var \PHPUnit\Framework\TestCase|null
*/
public $test;
/**
* Holds the tests repository.
*
@ -99,7 +106,13 @@ final class TestSuite
public static function getInstance(string $rootPath = null): TestSuite
{
if (is_string($rootPath)) {
return self::$instance ?? self::$instance = new TestSuite($rootPath);
self::$instance = new TestSuite($rootPath);
foreach (Plugin::$callables as $callable) {
$callable();
}
return self::$instance;
}
if (self::$instance === null) {

View File

@ -8,6 +8,7 @@ use Pest\PendingObjects\BeforeEachCall;
use Pest\PendingObjects\TestCall;
use Pest\PendingObjects\UsesCall;
use Pest\Support\Backtrace;
use Pest\Support\HigherOrderTapProxy;
use Pest\TestSuite;
use PHPUnit\Framework\TestCase;
@ -59,8 +60,12 @@ function uses(string ...$classAndTraits): UsesCall
*
* @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();
return new TestCall(TestSuite::getInstance(), $filename, $description, $closure);

12
stubs/Helpers.php Normal file
View File

@ -0,0 +1,12 @@
<?php
use Illuminate\Contracts\Auth\Authenticatable;
use Tests\TestCase;
/**
* Set the currently logged in user for the application.
*/
function actingAs(Authenticatable $user, string $driver = null): TestCase
{
return test()->actingAs($user, $driver);
}

View File

@ -47,6 +47,12 @@
✓ it catch exceptions
✓ 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
✓ it proxies calls to object
@ -89,6 +95,9 @@
PASS Tests\Playground
✓ basic
PASS Tests\Plugins\Traits
✓ it allows global uses
PASS Tests\Unit\Actions\AddsCoverage
✓ it adds coverage if --coverage exist
✓ it adds coverage if --min exist
@ -126,5 +135,5 @@
WARN Tests\Visual\Success
s visual snapshot of test suite on success
Tests: 6 skipped, 65 passed
Time: 2.50s
Tests: 6 skipped, 70 passed
Time: 2.68s

View File

@ -3,3 +3,13 @@
if (class_exists(NunoMaduro\Collision\Provider::class)) {
(new NunoMaduro\Collision\Provider())->register();
}
trait PluginTrait
{
public function assertPluginTraitGotRegistered(): void
{
assertTrue(true);
}
}
Pest\Plugin::uses(PluginTrait::class);

View 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()');

View File

@ -6,7 +6,7 @@ interface Foo
}
it('has bar', function () {
$mock = Mockery::mock(Foo::class);
$mock = mock(Foo::class);
$mock->shouldReceive('bar')
->times(1)
->andReturn(2);

8
tests/Helpers.php Normal file
View File

@ -0,0 +1,8 @@
<?php
use Mockery\MockInterface;
function mock(string $class): MockInterface
{
return Mockery::mock($class);
}

3
tests/Plugins/Traits.php Normal file
View File

@ -0,0 +1,3 @@
<?php
it('allows global uses')->assertPluginTraitGotRegistered();