Skip to content

Commit 4a83d99

Browse files
authored
Merge pull request #498 from cakephp/add-with-test-now
Add withTestNow() for scoped time mocking
2 parents d7ad1ae + afb3087 commit 4a83d99

2 files changed

Lines changed: 118 additions & 0 deletions

File tree

src/Chronos.php

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -327,6 +327,38 @@ public static function hasTestNow(): bool
327327
return static::$testNow !== null;
328328
}
329329

330+
/**
331+
* Temporarily sets "now" to the given value and executes the callback.
332+
*
333+
* After the callback is executed, the previous value of "now" is restored.
334+
* This is useful for testing time-sensitive code without affecting other tests.
335+
*
336+
* ### Example:
337+
*
338+
* ```
339+
* $result = Chronos::withTestNow('2023-06-15 12:00:00', function () {
340+
* return Chronos::now()->format('Y-m-d');
341+
* });
342+
* // $result === '2023-06-15'
343+
* ```
344+
*
345+
* @template T
346+
* @param \Cake\Chronos\Chronos|string|null $testNow The instance to use as "now".
347+
* @param callable(): T $callback The callback to execute.
348+
* @return T The return value of the callback.
349+
*/
350+
public static function withTestNow(Chronos|string|null $testNow, callable $callback): mixed
351+
{
352+
$previous = static::getTestNow();
353+
static::setTestNow($testNow);
354+
355+
try {
356+
return $callback();
357+
} finally {
358+
static::setTestNow($previous);
359+
}
360+
}
361+
330362
/**
331363
* Determine if there is just a time in the time string
332364
*

tests/TestCase/DateTime/TestingAidsTest.php

Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919
use Cake\Chronos\ChronosDate;
2020
use Cake\Chronos\Test\TestCase\TestCase;
2121
use DateTimeZone;
22+
use RuntimeException;
2223

2324
class TestingAidsTest extends TestCase
2425
{
@@ -231,4 +232,89 @@ public function testSetTestNowSingular()
231232

232233
$this->assertSame($c, Chronos::getTestNow());
233234
}
235+
236+
public function testWithTestNowSetsAndRestoresNull()
237+
{
238+
$this->assertNull(Chronos::getTestNow());
239+
240+
$result = Chronos::withTestNow('2023-06-15 12:00:00', function () {
241+
$this->assertNotNull(Chronos::getTestNow());
242+
$this->assertSame('2023-06-15', Chronos::now()->format('Y-m-d'));
243+
244+
return 'callback result';
245+
});
246+
247+
$this->assertSame('callback result', $result);
248+
$this->assertNull(Chronos::getTestNow());
249+
}
250+
251+
public function testWithTestNowRestoresPreviousTestNow()
252+
{
253+
$original = new Chronos('2020-01-01 00:00:00');
254+
Chronos::setTestNow($original);
255+
256+
Chronos::withTestNow('2023-06-15 12:00:00', function () {
257+
$this->assertSame('2023-06-15', Chronos::now()->format('Y-m-d'));
258+
});
259+
260+
$this->assertSame($original, Chronos::getTestNow());
261+
$this->assertSame('2020-01-01', Chronos::now()->format('Y-m-d'));
262+
}
263+
264+
public function testWithTestNowNested()
265+
{
266+
Chronos::setTestNow('2020-01-01 00:00:00');
267+
268+
Chronos::withTestNow('2021-06-15 00:00:00', function () {
269+
$this->assertSame('2021-06-15', Chronos::now()->format('Y-m-d'));
270+
271+
Chronos::withTestNow('2022-12-25 00:00:00', function () {
272+
$this->assertSame('2022-12-25', Chronos::now()->format('Y-m-d'));
273+
});
274+
275+
$this->assertSame('2021-06-15', Chronos::now()->format('Y-m-d'));
276+
});
277+
278+
$this->assertSame('2020-01-01', Chronos::now()->format('Y-m-d'));
279+
}
280+
281+
public function testWithTestNowRestoresOnException()
282+
{
283+
$original = new Chronos('2020-01-01 00:00:00');
284+
Chronos::setTestNow($original);
285+
286+
try {
287+
Chronos::withTestNow('2023-06-15 12:00:00', function () {
288+
throw new RuntimeException('Test exception');
289+
});
290+
$this->fail('Exception should have been thrown');
291+
} catch (RuntimeException $e) {
292+
$this->assertSame('Test exception', $e->getMessage());
293+
}
294+
295+
$this->assertSame($original, Chronos::getTestNow());
296+
}
297+
298+
public function testWithTestNowWithChronosInstance()
299+
{
300+
$testTime = new Chronos('2023-06-15 14:30:00');
301+
302+
$result = Chronos::withTestNow($testTime, function () {
303+
return Chronos::now()->format('Y-m-d H:i:s');
304+
});
305+
306+
$this->assertSame('2023-06-15 14:30:00', $result);
307+
$this->assertNull(Chronos::getTestNow());
308+
}
309+
310+
public function testWithTestNowWithNull()
311+
{
312+
Chronos::setTestNow('2020-01-01 00:00:00');
313+
314+
Chronos::withTestNow(null, function () {
315+
$this->assertNull(Chronos::getTestNow());
316+
});
317+
318+
$this->assertSame('2020-01-01', Chronos::now()->format('Y-m-d'));
319+
}
234320
}

0 commit comments

Comments
 (0)