gstarczyk/mimic

基于Java Mockito的PHP模拟和存根工具

2.0.2 2020-03-29 06:42 UTC

README

用于创建类模拟的简单工具。

它在Java工具Mockito的启发下开发。

目录

  1. 安装
  2. 要求
  3. 用法
  4. 示例

安装

要安装,只需将 'gstarczyk/mimic' 添加到您的 composer.json 中

composer require --dev gstarczyk/mimic

要求

Mimic 至少需要 PHP 5.5 版本。

用法

创建模拟

$mock = Gstarczyk\Mimic\Mimic::mock(SomeClass::class); 或如果导入 Mimic 类,则只需 $mock = Mimic::mock(SomeClass::class);

就这样,您只需创建模拟

您可以使用 InitMock 辅助函数,它使创建模拟更方便。

定义具有 @var@mock 注释的测试用例类属性,然后在每次测试之前运行 Mimic::initMocks($testCase)

对于任何定义并正确标记的属性,将生成模拟并将其分配给属性。

您还可以定义测试对象的属性并使用 @var@injectMocks 注释进行标记。测试对象将使用构造函数中的模拟创建。测试对象的类构造函数必须仅要求对象。这些所需的对象应定义为模拟。

class MyService
{
}

class MyOtherService
{
}

class MyServiceToTest
{
    /** @var MyService */
    private $service;

    /** @var MyOtherService */
    private $otherService;

    public function __construct(MyService $service, MyOtherService $otherService)
    {
        $this->service = $service;
        $this->otherService = $otherService;
    }
}

class TestCase extends \SomeFameousPhpTestFramework_TestCase
{
    /**
     * @var \MyNameSpace\MyServiceToTest
     * @injectMocks
     */
    private $testedObject;

    /**
     * @var \MyNameSpace\MyService
     * @mock
     */
    private $mock1;

    /**
     * @var \MyNameSpace\MyOtherService
     * @mock
     */
    private $mock2;

    protected function setUp()
    {
        Mimic::initMocks($this);
    }
}

遗憾的是,由于 PHP 注释的性质,您必须在 "var" 注释中使用 完整类名 或提供测试用例文件的路径(以扫描导入)。

class TestCase extends \SomeFameousPhpTestFramework_TestCase
{
    /**
     * @var MyServiceToTest
     * @injectMocks
     */
    private $testedObject;

    /**
     * @var MyService
     * @mock
     */
    private $mock1;

    /**
     * @var MyOtherService
     * @mock
     */
    private $mock2;

    protected function setUp()
    {
        Mimic::initMocks($this, __FILE__);
    }
}

定义存根

要定义存根(模拟对象的操作),请使用 Mimic::when() 方法。

$mock = Mimic::mock(SomeClass::class);
Mimic::when($mock)
    ->invoke('someMethod') // choose method
    ->withoutArguments()   // choose arguments
    ->willReturn('some value'); // define behaviour

要选择参数,您可以使用三种方法

  • withoutArguments() - 当不应传递任何参数时
  • withAnyArguments() - 当您不关心参数时
  • with(....) - 当您想指定为哪些参数定义行为时
                       equal() value matcher will be used to check arguments. 
                       You can refine matching by using value matchers in arguments list
                       (see examples).
    

要定义存根的行为,请使用以下列出的方法

  • willReturn(mixed $value) - 指定方法返回的值
  • willReturnCallbackResult(Closure $callback) - 动态准备返回值。调用方法时传递的参数将传递给 $callback。
  • willThrow(Exception $exception) - 在调用期间抛出异常

您可以为每个参数集定义不同的操作

Mimic::when($mock)
    ->invoke('someMethod')
    ->with(10, 20)
    ->willReturn('some value');
    
Mimic::when($mock)
    ->invoke('someMethod')
    ->with(100, 200)
    ->willReturnCallbackResult(function($arg1, $arg2) {
        return $arg1 + $arg2;
    });

您可以使用值匹配器来细化参数列表

Mimic::when($mock)
    ->invoke('someMethod')
    ->with(Match::anyInteger(), 20)
    ->willReturn('some value');

如果您想存根连续调用,则可以使用专门的存根构建器

Mimic::when($mock)
    ->consecutiveInvoke('someMethod')
    ->willReturn('first')
    ->thenReturn('second')
    ->thenReturn('third');

使用此构建器时,您不能定义参数,只能定义连续的行为(默认使用 withAnyArguments())。

方法调用的验证

要验证您的模拟对象的某个方法已被调用,请使用 Mimic::verify() 方法。

Mimic::verify($mock)
    ->method('someMethod')
    ->with(100, 200)
    ->wasCalled(Times::exactly(1));

同样,您可以使用值匹配器来细化参数列表

Mimic::verify($mock)
    ->method('someMethod')
    ->with(Match::anyInteger(), 200)
    ->wasCalled(Times::exactly(1));

您还可以验证连续调用

Mimic::verify($mock)
    ->consecutiveMethodInvocations('getMore')
    ->wasCalledWith('a')
    ->thenWith('b')
    ->thenWithAnyArguments()
    ->thenWith('c')
    ->thenWithoutArguments();

Mimic::verify($mock)
    ->consecutiveMethodInvocations('getMore')
    ->wasCalledWithoutArguments()
    ...
        

Mimic::verify($mock)
    ->consecutiveMethodInvocations('getMore')
    ->wasCalledWithAnyArguments()
    ...

可用的值匹配器

  • Match::equal($value) - 使用 "==" 运算符进行比较
  • Match::same($value) - 使用 "===" 运算符进行比较
  • Match::anyString()
  • Match::anyInteger()
  • Match::anyFloat()
  • Match::anyTraversable() - 匹配数组和实现了 \Traversable 的对象
  • Match::anyObject($className = null)
  • Match::stringStartsWith($prefix)
  • Match::stringEndsWith($prefix)

可用的调用次数验证器

  • Times::exactly($value)
  • Times::atLeast($value)
  • Times::once() - 与 Times::exactly(1) 相同
  • Times::never() - 与 Times::exactly(0) 相同

参数捕获

有时您需要对已验证方法的参数进行更复杂的断言。您可以使用参数捕获器,并在捕获的数据上使用您喜欢的断言工具。

$mock->someMethod('a');
$mock->someMethod('b');
$mock->someMethod('c', 'd');

$captor = new ArgumentsCaptor();
Mimic::verify($mock)
        ->method('someMethod')
        ->with($captor)
        ->wasCalled(Times::exactly(3));

$capturedData = $captor->getValues();
$expectedData = [
    ['a'],
    ['b'],
    ['c', 'd']
];
Assert::assertEquals($expectedData, $capturedData);

监视一个方法

有时您可能需要调用模拟的原始方法。

Mimic::spy($mock, 'someMethod');
$mock->someMethod('a', 'b');
// The method 'someMethod' of the original class will be call,
// The invocation is still be counted

示例

让我们假设我们有以下类

class Person
{
    private $birthDate;

    public function __construct(\DateTimeImmutable $birthDate)
    {
        $this->birthDate = $birthDate;
    }

    public function getAge()
    {
        $now = new \DateTimeImmutable('now');
        $age = $now->diff($this->birthDate);

        return $age->y;
    }
}

class AgeVerifier
{
    /**
     * @param Person $customer
     * @return bool
     */
    public function isAdult(Person $customer)
    {
        return $customer->getAge() >= 18;
    }
}

并且我们想测试 AgeVerifier::isAdult() 方法,但不需要在 Person::getAge() 中运行代码。

要做这样的“复杂”操作,我们需要

$person = Mimic::mock(Person::class);
$verifier = new AgeVerifier();
Mimic::when($person)
    ->invoke('getAge')
    ->withoutArguments()
    ->willReturn(20);
$result = $verifier->isAdult($person);

assert($result === true);

并且如果我们想验证 AgeVerifier 是否使用了 Person::getAge

$person = Mimic::mock(Person::class);
$verifier = new AgeVerifier();
$verifier->isAdult($person);

Mimic::verify($person)
    ->method('getAge')
    ->withoutArguments()
    ->wasCalled(Times::once());