如何模拟非确定性方法?

How to mock a non-deterministic method?

使用非确定性方法考虑 class:

class Foo
{
    public function getSome(): int
    {
        static $int = 0;
        return ++$int;
    }
}

如何使用 Mockery 模拟相同的行为?

class Bar
{
    public function useFoo(Foo $foo)
    {
        echo $foo->getSome() . ", ";
        echo $foo->getSome() . ", ";
        echo $foo->getSome();
    }
}

$mock = Mockery::mock(Foo::class);
$mock->shouldReceive('getSome')->andReturn(1);
$mock->shouldReceive('getSome')->andReturn(2);
$mock->shouldReceive('getSome')->andReturn(3);

// Should display 1, 2, 3
$bar = new Bar();
$bar->useFoo($mock);

另一个例子:

class Clock
{
    public function getTime(): int
    {
        return time();
    }
}

class Sleeper 
{
    public function sleep(int $seconds): void
    {
        sleep($seconds);
    }
}

如何使用 Mockery 模拟相同的行为?

class Stoper
{
    private Clock $clock;
    private Sleeper $sleeper;

    public function __construct(Clock $clock, Sleeper $sleeper)
    {
        $this->clock = $clock;
        $this->sleeper = $sleeper;
    }

    public function measure(int $seconds): int
    {
        $start = $this->clock->getTime();
        $this->sleeper->sleep($seconds);

        return $this->clock->getTime();
    }
}

$clockMock = Mockery::mock(Clock::class);
$clockMock->shouldReceive('getTime')->andReturn(1000000001);
$clockMock->shouldReceive('getTime')->andReturn(1000000011);

$sleeperMock = Mockery::mock(Sleeper::class);
$sleeperMock->shouldReceive('sleep');

$stoper = new Stoper($clockMock, $sleeperMock);
$this->assertEquals(10, $stoper->measure(10));

肮脏的解决方案

我发现我可以耍花招了

class ClockCaller
{
    public function getTime(int $callNumber)
    {
        return (new Clock())->getTime();
    }
}

class Stoper
{
    private ClockCaller $clockCaller;
    private Sleeper $sleeper;

    public function __construct(ClockCaller $clockCaller, Sleeper $sleeper)
    {
        $this->clockCaller = $clockCaller;
        $this->sleeper = $sleeper;
    }

    public function measure(int $seconds): int
    {
        $start = $this->clockCaller->getTime(1);
        $this->sleeper->sleep($seconds);

        return $this->clockCaller->getTime(2);
    }
}

$clockMock = Mockery::mock(ClockCaller::class);
$clockMock->shouldReceive('getTime')->withArgs([1])->andReturn(1000000001);
$clockMock->shouldReceive('getTime')->withArgs([2])->andReturn(1000000011);

不过好像很讨厌

Mockery 文档中的 Simple Example 给出了您问题的答案。应该 return 连续 10、12 和 14 度的温度服务可以这样模拟:

$service = Mockery::mock('service');
$service->shouldReceive('readTemp')
         ->times(3)
         ->andReturn(10, 12, 14);

因此,对于您的 Foo class:

$mock = Mockery::mock(Foo::class);
$mock->shouldReceive('getSome')
     ->times(3)
     ->andReturn(1, 2, 3);

还有你的Clock

$clockMock = Mockery::mock(Clock::class);
$clockMock->shouldReceive('getTime')
          ->times(2)
          ->andReturn(1000000001, 1000000011);

您可以使用 expectOutputString 检查输出:

public function testFoo(){
    $this->expectOutputString("foo");
    echo "foo";
}

关于睡眠功能,你可以使用\sleep()(全局命名空间),并覆盖它.. 另外,您可以使用 mocking built-in PHP functions

“如何模拟 non-deterministic 方法?”问题很笼统。

必须分别考虑每种方法。

第一个例子闻起来像上面的sayyid,划掉。

我们来看第二个例子

问题是时间不可预测,需要修复。

interface ClockInterface
{
    public function getTime(): int;
}

class Clock implements ClockInterface
{
    public function getTime(): int
    {
        return time();
    }
}

class ClockTest implements ClockInterface
{
    public function getTime(): int
    {
        return 0; // first unix ts
    }
}


class Stoper
{
    private ClockInterface $clock;
    private Sleeper $sleeper;

    public function __construct(ClockInterface $clock, Sleeper $sleeper)
    {
        $this->clock = $clock;
        $this->sleeper = $sleeper;
    }

    ...
}

单元测试:将 ClockTest class 作为 Stoper class 的第一个构造参数注入并解决问题。

如果创建包,ClockTest class 可以是 dist 部分,这是一个很好的做法 (https://github.com/Seldaek/monolog/blob/main/src/Monolog/Test/TestCase.php)