Featured image of post PHPUnit 測試含 sleep() 的程式:2 種 mock 解法比較

PHPUnit 測試含 sleep() 的程式:2 種 mock 解法比較

程式裡有 sleep() 讓每次測試都要多等幾秒,可抽出 Clock class 用 Mockery spy 替換,或用 php-mock 直接 mock 內建函式,無需改動原始碼。

程式裡有 sleep() 呼叫時,測試會跟著等,像下面這個 retry 邏輯,跑一次測試就要等 15 秒。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
// app/Http/Controllers/HomeController.php
namespace App\Http\Controllers;

class HomeController extends Controller
{
    public function index(): string
    {
        $tries = 3;
        while (true) {
            // do something
            sleep(5);

            $tries--;
            if ($tries <= 0) {
                break;
            }
        }

        return 'foo';
    }
}

方法一:抽出 Clock class 再用 mock 替換

sleep 包進一個 Clock class,測試時用 Mockery spy 替換掉。

1
2
3
4
5
6
7
8
9
namespace App;

class Clock
{
    public function sleep(int $second): void
    {
        sleep($second);
    }
}

Controller 改成注入 Clock

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
// app/Http/Controllers/HomeController.php
namespace App\Http\Controllers;

use App\Clock;

class HomeController extends Controller
{
    public function index(Clock $clock): string
    {
        $tries = 3;
        while (true) {
            // do something
            $clock->sleep(5);

            $tries--;
            if ($tries <= 0) {
                break;
            }
        }

        return 'foo';
    }
}

測試用 swap 抽換:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
namespace Tests\Feature;

use App\Clock;
use Mockery as m;
use Tests\TestCase;

class ExampleTest extends TestCase
{
    public function test_sleep(): void
    {
        $clock = m::spy(Clock::class);
        $this->swap(Clock::class, $clock);

        $this->get('/')
            ->assertOk()
            ->assertSee('foo');

        // 驗證 sleep 執行次數
        $clock->shouldHaveReceived('sleep')->times(3);
    }
}

這個做法的好處是可以用 spy 驗證 sleep 被呼叫了幾次。

方法二:用 php-mock 直接 mock 內建函式

如果不想改動原本的程式碼,可以用 php-mock 這個 package。Controller 完全不用動,只改測試:

1
composer require --dev php-mock/php-mock
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
namespace Tests\Feature;

use phpmock\environment\SleepEnvironmentBuilder;
use Tests\TestCase;

class ExampleTest extends TestCase
{
    protected function setUp(): void
    {
        parent::setUp();
        $builder = new SleepEnvironmentBuilder();
        // sleep 所在的 namespace
        $builder->addNamespace('App\Http\Controllers');
        $this->environment = $builder->build();
        $this->environment->enable();
    }

    protected function tearDown(): void
    {
        parent::tearDown();
        $this->environment->disable();
    }

    public function test_sleep(): void
    {
        $this->get('/')
            ->assertOk()
            ->assertSee('foo');
    }
}

php-mock 有個限制:sleep 必須在某個 namespace 底下才能被 mock。如果 sleep 是在全域 namespace 呼叫的,這招就不管用了。