Featured image of post PHPUnit: Mock sleep() in 2 Ways Without Waiting

PHPUnit: Mock sleep() in 2 Ways Without Waiting

When sleep() slows PHPUnit tests, extract a Clock class and use Mockery spy to replace it, or use php-mock to mock the built-in function with no code changes.

When your code contains sleep() calls, tests have to wait too. For example, this retry logic makes each test run take 15 seconds:

 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';
    }
}

Approach 1: Extract a Clock Class and Mock It

Wrap sleep in a Clock class and replace it with a Mockery spy in tests.

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

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

Inject Clock into the controller:

 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 it in the test:

 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');

        // verify sleep was called the expected number of times
        $clock->shouldHaveReceived('sleep')->times(3);
    }
}

The advantage of this approach is that you can use the spy to verify how many times sleep was called.

Approach 2: Mock the Built-in Function with php-mock

If you don’t want to modify the original code, use php-mock. The controller stays untouched — only the test changes:

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();
        // namespace where sleep() is called
        $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 has a limitation: sleep must be called within a namespace to be mockable. If sleep is called in the global namespace, this approach won’t work.