krakjoe/mimus

dev-master 2021-01-29 11:13 UTC

This package is auto-updated.

Last update: 2024-09-19 23:01:41 UTC


README

Build Status Coverage Status

要求

双倍

测试双倍是一个在系统测试期间取代正式类型对象的对象

<?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::classDouble::make将尝试根据传递给构造函数的$name返回一个缓存的值,它们在检索值时可以选择性地进行$reset

从第一次调用Double::getInstanceDouble::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;
	}
}

待办事项

  • 还需要更多测试...
  • 我一直想见一只北极熊,最好是刚出生的小熊...