unit testing
random results
testing strategies
software development
test automation

Unit Testing with functions that return random results

Master System Design with Codemia

Enhance your system design skills with over 120 practice problems, detailed solutions, and hands-on exercises.

Introduction

Functions that return random results are only hard to test when the randomness is mixed directly into the logic you care about. The standard fix is to control the random source during tests and then assert deterministic outcomes or invariant properties. Once randomness becomes an input, the function becomes testable again.

Inject the Random Source

A clean design takes a random generator or callback as a dependency.

python
1import random
2
3
4def choose_winner(players, rng=None):
5    rng = rng or random.Random()
6    index = rng.randrange(len(players))
7    return players[index]

In production, the function still behaves randomly. In tests, you can pass a seeded generator.

python
1def test_choose_winner_is_repeatable_with_seed():
2    import random
3
4    rng = random.Random(123)
5    players = ["Ava", "Ben", "Chris"]
6
7    assert choose_winner(players, rng) == "Ava"

The test is deterministic because the random source is deterministic.

Use Fakes for Exact Branch Control

Sometimes a seed is still more indirect than you want. A fake random object makes the test intention explicit.

python
1class FakeRng:
2    def randrange(self, upper_bound):
3        return 2
4
5
6def test_choose_winner_uses_rng_result():
7    players = ["Ava", "Ben", "Chris"]
8    assert choose_winner(players, FakeRng()) == "Chris"

This is especially helpful when the production code calls several random methods and you want total control over the path.

Test Invariants Instead of Exact Outcomes

Some properties should be true regardless of the random value. Those are often better unit tests than one exact seeded output.

python
1def test_choose_winner_always_returns_a_known_player():
2    import random
3
4    players = ["Ava", "Ben", "Chris"]
5    for seed in range(20):
6        winner = choose_winner(players, random.Random(seed))
7        assert winner in players

This checks a real contract: the function never invents a result outside the input set.

Separate Randomness From Business Logic

If a function both generates random numbers and applies complex business rules, split those concerns.

python
1def roll_index(count, rng):
2    return rng.randrange(count)
3
4
5def choose_by_index(players, index):
6    return players[index]

Now choose_by_index has no randomness at all, so the core business behavior can be tested with ordinary deterministic unit tests.

Statistical Tests Are a Different Category

Sometimes you do want to verify approximate fairness or distribution. That is a different kind of test and should usually live outside the smallest unit-test layer.

python
1from collections import Counter
2import random
3
4
5def test_distribution_is_roughly_balanced():
6    players = ["Ava", "Ben", "Chris"]
7    counts = Counter(choose_winner(players, random.Random(i)) for i in range(3000))
8
9    for value in counts.values():
10        assert 800 <= value <= 1200

Keep thresholds broad if you do this. Statistical assertions are inherently noisier than ordinary unit tests.

A Small Dice Example

python
1import random
2
3
4def roll_die(rng=None):
5    rng = rng or random.Random()
6    return rng.randint(1, 6)
7
8
9def test_roll_die_stays_in_range():
10    for seed in range(50):
11        value = roll_die(random.Random(seed))
12        assert 1 <= value <= 6

This test verifies the real guarantee without pretending the same face must always appear in production.

Common Pitfalls

  • Calling the global random generator directly inside business logic and then writing brittle tests against exact outputs.
  • Using a seed when a fake RNG would express the test intention more clearly.
  • Treating a fairness check as though it were a perfectly deterministic unit test.
  • Asserting too little, such as only the return type, instead of the real contract.
  • Leaving randomness entangled with business logic so the important rules remain hard to test.

Summary

  • Inject randomness so tests can control it.
  • Use seeded generators or fake RNG objects for deterministic test cases.
  • Assert invariants such as valid ranges and membership, not only exact outputs.
  • Separate random-number generation from business logic when possible.
  • Treat fairness checks as statistical tests, not as ordinary unit tests.

Course illustration
Course illustration

All Rights Reserved.