krakjoe / mimus
Requires
- php: ^7.2|^8.0
- ext-componere: ^3.0.0
Requires (Dev)
- php-coveralls/php-coveralls: ^2.1
- phpunit/phpunit: ^8.0 || ^9.0
This package is auto-updated.
Last update: 2024-09-19 23:01:41 UTC
README
要求
- PHP 7.2+
- Componere
双倍
测试双倍是一个在系统测试期间取代正式类型对象的对象
<?php require "vendor/autoload.php"; use \mimus\Double as double; class Foo { public function doesSomethingAndReturnsBool() : bool { /** ... **/ return true; } } $builder = double::class(Foo::class); $object = $builder->getInstance(); ?>
此时,$object 是 instanceof Foo
,与声明 Foo
具有相同的接口,但其方法都不做任何事情 - 它们已经被桩化。
需要注意的是,虽然 mimus 支持一个熟悉的模式(getInstance
)以允许注入依赖项,但 mimus 在内部已经替换了 Foo
的声明,因此后续对 new Foo
的调用将创建一个测试双倍,使得上面的代码和下面的代码在功能上是等效的
<?php require "vendor/autoload.php"; use \mimus\Double as double; class Foo { public function doesSomethingAndReturnsBool() : bool { /** ... **/ return true; } } $builder = double::class(Foo::class); $builder->commit(); /* would be committed on getInstance() or rule() */ $object = new Foo(); ?>
由于在调用 double::class 时 Foo
已经声明,仅使用用户空间的 PHP 独立实现此行为是不可能的:这就是 mimus 必须依赖于 Componere 的原因,这也是 mimus 与任何其他 PHP 模拟框架之间的主要区别之一。
桩
要使桩执行某些操作,您必须告诉 mimus 该方法应该或将要做什么
<?php require "vendor/autoload.php"; use \mimus\Double as double; class Foo { public function doesSomethingAndReturnsBool() : bool { /** ... **/ return true; } } $builder = double::class(Foo::class); $builder->rule("doesSomethingAndReturnsBool") ->expects() /* take any arguments */ ->returns(true); /* return true; */ $object = $builder->getInstance(); var_dump($object->doesSomethingAndReturnsBool()); // bool(true) ?>
在某些情况下,我们的方法需要针对不同的输入返回不同的值
<?php require "vendor/autoload.php"; use \mimus\Double as double; class Foo { public function doesSomethingAndReturnsBool($argument) : bool { /** ... **/ return true; } } $builder = double::class(Foo::class); $builder->rule("doesSomethingAndReturnsBool") ->expects(true) /* takes these arguments */ ->returns(true); /* return true; */ $builder->rule("doesSomethingAndReturnsBool") ->expects(false) /* takes these arguments */ ->returns(false); /* return false; */ $object = $builder->getInstance(); var_dump($object->doesSomethingAndReturnsBool(true)); // bool(true) var_dump($object->doesSomethingAndReturnsBool(false)); // bool(false) ?>
此时,我们已根据运行时提供的参数定义了两个通过该方法的合法路径,如果方法被这样调用
<?php var_dump($object->doesSomethingAndReturnsBool("mimus"));
mimus 将为每个已破坏的规则引发 \mimus\Exception
(2)。
路径
路径可以
- 期望(或设置)一个返回值(前面的示例)
- 执行原始实现
- 执行不同的实现
- 期望抛出异常
- 期望最多被进入的次数(或从未进入)
执行原始实现
假设我们希望允许原始实现执行,并确保返回值符合预期
<?php require "vendor/autoload.php"; use \mimus\Double as double; class Foo { public function doesSomethingAndReturnsBool($arg) : bool { /** ... **/ return true; } } $builder = double::class(Foo::class); $builder->rule("doesSomethingAndReturnsBool") ->expects("yes") ->executes() // executes original ->returns(true); $builder->rule("doesSomethingAndReturnsBool") ->expects("no") ->executes() // executes original ->returns(false); $object = $builder->getInstance(); var_dump($object->doesSomethingAndReturnsBool("yes")); // bool(true) var_dump($object->doesSomethingAndReturnsBool("no")); ?>
虽然第一次调用将成功,但第二次将引发 \mimus\Exception: return value expected to be bool(false), got bool(true)
。
执行不同实现
假设我们希望用替代原始实现
<?php require "vendor/autoload.php"; use \mimus\Double as double; class Foo { public function doesSomethingAndReturnsBool($arg) : bool { /** ... **/ return true; } } $builder = double::class(Foo::class); $builder->rule("doesSomethingAndReturnsBool") ->expects("yes") ->executes() // executes original code ->returns(true); $builder->rule("doesSomethingAndReturnsBool") ->expects("no") ->executes(function(){ return false; }); // no need for returns() $object = $builder->getInstance(); var_dump($object->doesSomethingAndReturnsBool("yes")); // bool(true) var_dump($object->doesSomethingAndReturnsBool("no")); // bool(false) ?>
虽然第一次调用将调用原始实现,但第二次将调用给定的实现。
异常
假设我们想验证路径抛出异常
<?php require "vendor/autoload.php"; use \mimus\Double as double; class Foo { public function doesSomethingAndReturnsBool($arg) : bool { if ($arg) { throw new Exception(); } return true; } } $builder = double::class(Foo::class); $builder->rule("doesSomethingAndReturnsBool") ->expects(true) ->executes() ->throws(Exception::class); $builder->rule("doesSomethingAndReturnsBool") ->expects(false) ->executes() ->throws(Exception::class); $object = $builder->getInstance(); try { $object->doesSomethingAndReturnsBool(true); } catch (Exception $ex) { } $object->doesSomethingAndReturnsBool(false); ?>
虽然第一次调用将成功并捕获结果异常,但第二次将引发(未捕获的):mimus\Exception: expected exception of type Exception, nothing thrown
。
限制
假设我们想限制方法进入的次数
<?php require "vendor/autoload.php"; use \mimus\Double as double; class Foo { public function doesSomethingAndReturnsBool() : bool { /* ... */ return true; } } $builder = double::class(Foo::class); $builder->rule("doesSomethingAndReturnsBool") ->expects(true) ->returns(true) ->once(); // limit() and never() also available $object = $builder->getInstance(); var_dump($object->doesSomethingAndReturnsBool(true)); // bool(true) var_dump($object->doesSomethingAndReturnsBool(true)); ?>
虽然第一次调用将成功,但第二次将引发:mimus\Exception: limit of 1 exceeded
。
部分模拟
部分模拟用于,例如,允许模拟类型的对象执行实现
<?php require "vendor/autoload.php"; use \mimus\Double as double; interface IFace { public function interfaceMethod(); } class Foo implements IFace { public function interfaceMethod() { return true; } public function nonInterfaceMethod() { return false; } } $builder = double::class(Foo::class); $builder->partialize([ "interfaceMethod" ]); $builder->rule("nonInterfaceMethod") ->expects() ->never(); $object = $builder->getInstance(); var_dump($object->interfaceMethod()); // bool(true) var_dump($object->nonInterfaceMethod());
虽然第一次调用将按实现执行,但第二次将引发 mimus\Exception: limit of 1 exceeded
。
double::partialize
还接受有效类的名称,上面的调用可以写成
/* ... */ $builder->partialize(IFace::class); /* ... */
接口
有时,在没有实现的情况下模拟一个接口是有用的,我们可以为此使用测试双倍
<?php require "vendor/autoload.php"; use mimus\Double as double; interface IFace { public function publicMethod(); } $builder = double::make(myinterfaces::class, [ [ IFace::class ] ]); $builder->rule("publicMethod") ->expects() ->executes(function(){ return true; }); $object = $builder->getInstance(); var_dump($object->publicMethod()); // bool(true)
该 $object
将是 instanceof IFace
,名称为 myinterfaces
。
可以使用 Double::implements
方法在构造之后向双倍添加接口。
特质
特性和编译器处理可复制的代码单元一样;当类声明中有use
时,特质的接口会被粘贴到当前声明中,使得内联声明将覆盖特质的声明。
对于模拟,我们希望使用特质的方式有所不同:我们希望在类声明之上进行粘贴,使得特质成为实现的真实来源。
<?php require "vendor/autoload.php"; use \mimus\Double as double; class Foo { public function doesSomethingAndReturnsBool() : bool { /** ... **/ return true; } } trait FooDoubleMethods { public function doesSomethingAndReturnsBool() : bool { return false; } } $builder = double::class(Foo::class); $builder->use(FooDoubleMethods::class); $builder->rule("doesSomethingAndReturnsBool") ->expects() ->executes(); $object = $builder->getInstance(); var_dump($object->doesSomethingAndReturnsBool()); // bool(false) ?>
注意,use
并不意味着双重应该被部分化。
双重生命周期
命名构造函数Double::class
和Double::make
将尝试根据传递给构造函数的$name
返回一个缓存的值,它们在检索值时可以选择性地进行$reset
。
从第一次调用Double::getInstance
或Double::rule
开始,类在引擎中以提供的$name
存在于其中;某些操作,如实现接口和使用特质,将不再可能,必须在这些调用之前执行。
类将一直存在,直到使用Double::unlink
显式移除:当移除双重时,任何被它替换的类都将恢复到其原始实现。
API
<?php namespace mimus { class Double { /* * Shall create or return mock by name * @param string the name of the class to mock * @param bool optionally prohibit resetting rules * @throws LogicException if name does not exist * @throws LogicException if name is the name of an abstract class */ public static function class(string $name, bool $reset = true) : Double; /* * Shall create or return mock by name * @see \Componere\Definition::__construct */ public static function make(string $name, mixed $args, bool $reset = true) : Double; /* * Shall delete a mock by name * @param name of mock * @throws LogicException if mock does not exist */ public static function unlink(string $name) : void; /* * Shall check if a mock exists * @param name of mock */ public static function exists(string $name) : bool; /* * Shall delete all mocks */ public static function clear() : void; /* * Shall implement the given interface * @param name of interface * @param optionally partialize on interface * @throws LogicException if invoked after rule() or getInstance() * @throws LogicException if not a valid interface */ public function implements(string $interface, bool $partialize = false) : Double; /* * Shall use the given trait * @param name of trait * @param optionally partialize on trait * @throws LogicException if invoked after rule() or getInstance() * @throws LogicException if not a valid trait */ public function use(string $interface, bool $partialize = false) : Double; /* * Shall turn this into a partial by allowing execution of the given methods */ public function partialize(array $methods = []) : Double; /* * Shall turn this into a partial by allowing execution * of the methods in the given class */ public function partialize(string $class) : Double; /* * Shall turn this into a partial by allowing execution * of the methods in the given class with exceptions */ public function partialize(string $class, array $except = []) : Double; /* * Shall define or redefine the constant with name */ public function defines(string $name, $value) : Double; /* * Shall ensure the class is available by name * Note: until the first call to rule() or getInstance() the class is not registered * this method serves the case where no rule() or getInstance() call is made * in the current scope. */ public function commit() : void; /* * Shall create a new Rule for method * @param string the name of the method * @throws LogicException if the method does not exist */ public function rule(string $method) : Rule; /* * Shall clear all the rules for the given method * @param string the name of a method, or null * Note: if method is null, rules for all methods are reset */ public function reset(string $method = null); /* * Shall return an object of the mocked type * Note: if not arguments are passed, no constructor is invoked */ public function getInstance(...$args) : object; } class Rule { /* * Shall return the path for the given arguments, or any arguments if none are given */ public function expects(...$args) : Path; } class Path { /* * Shall tell mimus to execute something for this path * @param Closure * If no Closure is passed, the original method is allowed to execute * If a Closure is passed, it is executed in place of the original method * Note: Closure should be compatible with function(Closure $prototype, ...$args) * Closure is bound to the correct scope before invocation * If Path::executes is not invoked, nothing will be executed for this Path */ public function executes(Closure $closure = null) : Path; /* * Shall tell mimus what this path should (or will) return * @param mixed * If this path executes, then the return value given is verified to * match the runtime return value. * If this path does not execute, the return value is used as the * runtime return value. * @throws LogicException if this Path is void * Note: If Path::returns is not invoked, any return is allowed for this Path */ public function returns($value) : Path; /* * Shall tell mimus that this path should be void (not return anything) * @throws LogicException if this Path returns */ public function void() : Path; /* * Shall tell mimus what this path should throw * @param string the name of the exception expected * @throws LogicException for non executable Path * Note: If Path::throws is not invoked, any exception is allowed for this Path */ public function throws(string $class) : Path; /* * Shall tell mimus that this path should never be travelled */ public function never() : Path; /* * Shall tell mimus that this path should only be travelled once */ public function once() : Path; /* * Shall tell mimus that this path should be travelled a maximum number of times */ public function limit(int $times) : Path; /* * Shall tell mimus to add a validator to Path * Note: Validators will be executed after all other conditions before returning, * Validators will be bound to the correct object before invocation * Validators that return false will raise exceptions * Validators should have the prototype function($retval = null) */ public function validates(\Closure $validator) : Path; } }
待办事项
- 还需要更多测试...
- 我一直想见一只北极熊,最好是刚出生的小熊...