Safeguard
Developer Tools•

Deterministic tests: control clocks, randomness and seeds

How to make tests of tokens, expiry and random values repeatable by injecting a clock and a random source, without weakening production randomness.

Safeguard Security Team
3 min read

Make time and randomness inputs to your code, not hidden calls. Inject a clock and a random source so tests can fix them, and keep real cryptographic randomness in production. Then expiry, rate limits and token logic can be tested exactly, and failures can be replayed.

Why this matters for security tests

Many security features depend on time: token expiry, lockout windows, rate limits, certificate validity, one time code windows. Testing them by sleeping is slow and flaky. Testing them by hoping the run lands in the right second is worse.

Random values cause the opposite problem. A test that generates a random token and sometimes hits an edge case fails once a month and nobody can reproduce it.

Inject the clock

Give the code a small interface with a method that returns the current time. Production passes the system clock. Tests pass a fake that you can move forward.

class FakeClock:
    def __init__(self, now): self.now = now
    def time(self): return self.now
    def advance(self, seconds): self.now += seconds

def test_token_expires():
    clock = FakeClock(1_000)
    tokens = TokenService(clock, ttl=60)
    t = tokens.issue("u1")
    clock.advance(59)
    assert tokens.verify(t)
    clock.advance(2)
    assert not tokens.verify(t)

The test checks both sides of the boundary, which is where off by one errors in expiry live.

Inject randomness, carefully

For logic that uses randomness only to pick among options, pass a seeded generator in tests. For anything that must be unpredictable, such as session ids, reset tokens and keys, keep the cryptographic source in production and assert properties in tests: length, character set, uniqueness across many calls.

Do not make the production code seedable through configuration. A seed knob that exists "for testing" tends to survive into real deployments.

Handle test ordering and shared state

Reset global state between tests, and give each test its own database schema or transaction. The Google Testing Blog lists concurrency and reliance on non-deterministic behavior among root causes of flaky tests, and order dependence and shared state are typical examples.

Log the seed

If you use randomized test data or property tests, print the seed or the failing case when a test fails, so a developer can replay it. Many property testing libraries have a replay feature for this purpose.

Time zones and clock skew

Run a test with a non UTC time zone, and one that crosses a daylight saving change. Add a skew test for signature and token checks: a few seconds of tolerance should pass, and a large drift should fail.

A short checklist

  • Every test sets its own clock and does not read the real one.
  • No sleeps longer than a few milliseconds.
  • Randomized tests print their seed on failure.
  • Tests pass in any order and in parallel.
  • Time zone and skew cases exist for token and signature checks.

Quick check

Can I just mock the system time globally?

You can, and it works for small cases, but global patches leak between tests and hide the dependency. An injected clock is more explicit and safer.

Sources

Last reviewed: 19 October 2026.

Never miss an update

Weekly insights on software supply chain security, delivered to your inbox.

Self-healing security runs on Safeguard.

Your first fix PR is minutes away.

No sales call required, even your agent can complete the purchase over MCP.