Skip to content

Time Mocking

BDD::Behave ships with Timecop-style helpers for freezing and traveling through time inside an example. They work by wrapping Raku's now, Instant.now, DateTime.now, and Date.today so the wrap consults a dynamically-scoped freeze. When no freeze is active, the wrappers fall through to the real time source — they have no effect outside a freeze block.

freeze-time

1
2
3
4
5
6
7
8
freeze-time {
  # `now`, `Instant.now`, `DateTime.now`, and `Date.today` are all frozen
  # at the moment the block began.
  my $a = now;
  sleep 0.5;
  my $b = now;
  expect($a).to.eq($b);
};

Freeze at an explicit moment with a DateTime, Instant, or ISO 8601 string:

1
2
3
4
5
6
7
freeze-time DateTime.new('2024-01-01T00:00:00Z'), {
  expect(DateTime.now.year).to.eq(2024);
};

freeze-time '2030-06-15T12:00:00Z', {
  expect(DateTime.now.year).to.eq(2030);
};

Date.today is also frozen to the same instant:

1
2
3
4
5
freeze-time DateTime.new('2024-07-04T12:00:00Z'), {
  expect(Date.today.year).to.eq(2024);
  expect(Date.today.month).to.eq(7);
  expect(Date.today.day).to.eq(4);
};

travel-to

travel-to is a synonym for the explicit-moment form of freeze-time:

1
2
3
travel-to DateTime.new('2026-03-15T00:00:00Z'), {
  expect(DateTime.now.year).to.eq(2026);
};

travel-by

Inside an active freeze block, travel-by advances the frozen instant forward by a Duration (or a plain Real number of seconds):

1
2
3
4
5
6
7
freeze-time DateTime.new('2024-01-01T00:00:00Z'), {
  expect(DateTime.now.hour).to.eq(0);

  travel-by(Duration.new(3600));

  expect(DateTime.now.hour).to.eq(1);
};

Multiple advances compose:

1
2
3
4
5
6
freeze-time $start, {
  travel-by(10);
  travel-by(20);
  travel-by(30);
  # frozen instant is now $start + 60 seconds
};

Calling travel-by outside an active freeze dies with a clear message.

Nested freezes

Inner freezes shadow outer ones. When the inner block exits, the outer freeze is restored:

1
2
3
4
5
6
7
8
9
freeze-time DateTime.new('2020-01-01T00:00:00Z'), {
  expect(DateTime.now.year).to.eq(2020);

  freeze-time DateTime.new('2030-06-15T00:00:00Z'), {
    expect(DateTime.now.year).to.eq(2030);
  };

  expect(DateTime.now.year).to.eq(2020);
};

Restoration

Time is always restored when a freeze block exits, including when the block throws. The frozen state never leaks to the next example.

:freeze-time metadata

Examples and groups can opt into a freeze via the :freeze-time metadata. The runner wraps the example body in a freeze around before-each / after-each hooks — wall-clock duration is measured outside the freeze, so the example's started-at / finished-at / duration accessors still report real time.

Freeze at the moment the example body begins:

1
2
3
4
5
6
it 'freezes at start', :freeze-time, {
  my $a = now;
  sleep 0.1;
  my $b = now;
  expect($a).to.eq($b);
};

Freeze at an explicit moment:

1
2
3
it 'freezes at 2024-01-01', :freeze-time(DateTime.new('2024-01-01T00:00:00Z')), {
  expect(DateTime.now.year).to.eq(2024);
};

Pass :freeze-time(False) to explicitly opt out (useful when overriding an inherited group-level freeze).

current-time

current-time returns the current Instant — frozen when a freeze is active, real now otherwise. Use it from helpers that want to read "time" without bypassing the freeze.

1
2
3
freeze-time $instant, {
  expect(current-time()).to.eq($instant);
};