---
title: 'Mocking and Test Data'
source: 'https://academia.sh/en/courses/ruby-production/mocking-and-test-data'
course: 'The Ruby Ecosystem and Production'
language: en
updated: '2026-10-21T05:03:44+00:00'
license: 'CC BY-SA 4.0'
objectives:
    - 'Measures that the environment determines the result of a test that reads the clock and that passing the clock as a parameter turns this into code'
    - 'Separates the recording fake, the canned response and the expectation object from one another'
    - 'Shows that a counter-based test data generator gives a different value on each call and that isolation divides the generator among tests'
    - 'Measures that mocking removes one determinant while seeded test data leaves the order determinant'
---

# Mocking and Test Data

Why does a test that reads the clock fail tomorrow? The lesson measures passing the clock in from outside, fake objects and seeded test data, and separates which determinant mocking removes and which it leaves.

The previous lesson showed that a test suite fails depending on the order, and removed the failure through isolation. Now there is another input: today's date. A test passes today, fails thirty days later, and not a single line of its code has changed. The observation of this lesson is the `clock-reading test` row: the base is `true` and the evidence column shows `D- O- E+`. The result changes when the clock changes; it does not change when the dependency or the order changes.

M13/K03 asked how long a value stays: information held in the session was still there at the next request. Here the same question is applied to tests: after which test is a piece of test data still there? The answer depends on whose hands the data is in, and this lesson measures it. The core's `determinant.rb` file is given again exactly; the block is source and is pulled in with `require_relative`.

```ruby
# determinant.rb — a run model and a helper that classifies what determines a result

# Run: the three inputs a result may depend on. Everything outside these three is code.
Run = Data.define(:lock, :seed, :environment)

BASE = Run.new(
  lock: { "formatter" => "1.4.2", "parser" => "2.0.1" },
  seed: 1,
  environment: { clock: Time.utc(2026, 10, 16, 9, 0), cores: 4, variables: {} }
)

# Each variant changes a single input; the others stay as in the base.
VARIANTS = {
  dependency: BASE.with(lock: { "formatter" => "1.5.0", "parser" => "2.1.0" }),
  order: BASE.with(seed: 2),
  environment: BASE.with(environment: { clock: Time.utc(2027, 3, 1, 9, 0), cores: 8,
                                        variables: { "WORKER_COUNT" => "3" } })
}

# Rule: the computation runs first in the base, then once in each variant. The first variant
# that changes the result (dependency, order, environment, in that order) is the determinant;
# if none changes it, code.
def determinant(computation)
  base = computation.call(BASE)
  changed = VARIANTS.to_h { |name, run| [name, computation.call(run) != base] }
  kind = changed.key(true)&.to_s || "code"
  evidence = changed.map { |name, c| "#{name[0].upcase}#{c ? '+' : '-'}" }.join(" ")
  [kind, evidence, base]
end
```

## The Clock-Reading Test

The computation of the observation is a small rule: the trial period is 30 days, and the period is measured by the number of seconds elapsed since the start. The computation takes the clock as a parameter; two boundary values are measured.

```ruby
# clock.rb — the trial period is 30 days
def in_trial?(start, clock) = clock - start < 30 * 86_400

start = Time.utc(2026, 10, 1)
p in_trial?(start, Time.utc(2026, 10, 30, 23, 59))
p in_trial?(start, Time.utc(2026, 10, 31, 0, 0))
```
```
true
false
```

The boundary is `30 * 86_400` seconds. October 30 at 23:59 is within the period; October 31 at 00:00 is exactly thirty days later and gives `false`. This computation is pure: the same two inputs always give the same result. So far nothing depends on the environment. The dependency begins when the question is asked **where** the clock comes from.

The file below writes two tests. The first reads the clock from the run model's environment, that is, it takes the clock as the machine reports it. The second runs the same computation with a fixed clock source given up front.

```ruby
# environment.rb — who provides the clock determines the result
require_relative "determinant"

def in_trial?(start, clock) = clock - start < 30 * 86_400

start = Time.utc(2026, 10, 1)
p in_trial?(start, BASE.environment[:clock])
p in_trial?(start, VARIANTS[:environment].environment[:clock])
p determinant(->(r) { in_trial?(start, r.environment[:clock]) })
```
```
true
false
["environment", "D- O- E+", true]
```

With the clock in the base the test passed; with the clock in March 2027 it failed. The rule said `environment`, with the evidence `D- O- E+`: the result stayed the same in the dependency and order variants and changed in the environment variant. Without a single line of code changing, the same test gave two results. This is the test that reads the clock and will fail tomorrow: the clock read with `Time.now` behaves like the base in today's run and like March thirty days later. **PG19.** This measurement reads the clock from the environment model; the real clock's value is read with `Time.now` and is not printed because it changes from sandbox to sandbox.

## Making the Clock a Parameter

The fix is to take the clock out of the computation and pass it in from outside. When the computation takes the clock source as a parameter, who chooses the clock moves to the caller. In production the source is the real clock; in the test the source is a fixed clock. This fixed clock is a **fake**: an object that stands in for the real one and whose behavior the test controls.

```ruby
# fake_clock.rb — a test that passes the clock in from outside
require_relative "determinant"

def in_trial?(start, clock) = clock - start < 30 * 86_400
def trial_remaining?(start, clock_source) = in_trial?(start, clock_source.call)

start = Time.utc(2026, 10, 1)
fake_clock = -> { Time.utc(2026, 10, 16, 9, 0) }

p determinant(->(r) { trial_remaining?(start, -> { r.environment[:clock] }) })
p determinant(->(_r) { trial_remaining?(start, fake_clock) })
```
```
["environment", "D- O- E+", true]
["code", "D- O- E-", true]
```

The first line classifies the result as `environment` when the clock source comes from the run's environment. The second line replaces the source with a fixed clock and the result becomes `code`, with the evidence `D- O- E-`. The computation is still the same, the test still makes the same assertion; what changed is that the clock is chosen by the test. The clock is no longer something the environment says but the test's own input. **PG20.** The fake clock was built with a lambda; the way a real framework freezes time, for example by temporarily replacing the `Time.now` method, has a different scope and was not measured here.

Making it a parameter has a cost, and the measurement does not leave it out. The computation now expects a clock source; every place that calls it has to pass one, and since the source passed in production is the real clock, the path that works with the real clock never runs in the test. A test written with a fake clock verifies the computation; it does not verify that the clock is read correctly from the real source. For this reason faking the clock protects the clock-reading test from failing tomorrow but leaves the reading line unchecked. A good separation is to gather the reading in a single place and to try that place separately.

A fixed clock has a second benefit: boundary values become testable. With the real clock, catching the last minute of the 30th day requires waiting for that minute; with a fake clock two calls are enough, and the separation of `23:59` and `00:00` in the first measurement comes out the same in every run.

## Fake Objects

The clock is the simplest dependency to fake. Three kinds of fake objects are seen in tests, and the job of each is different. A **recording fake** does not do the work of the real one; it only records what it was told. A **canned response** returns a ready answer to a given call. An **expectation object** checks afterward whether a call was made.

```ruby
# notifier.rb — a recording fake
class FakeNotifier
  attr_reader :sent

  def initialize
    @sent = []
  end

  def deliver(recipient, text)
    @sent << [recipient, text]
  end
end

def notify_invoice(notifier, recipient) = notifier.deliver(recipient, "invoice ready")

notifier = FakeNotifier.new
notify_invoice(notifier, "a@example.test")
p notifier.sent
```
```
[["a@example.test", "invoice ready"]]
```

The invoice notification did not send a real message; the fake notifier wrote what was sent to an array. The test can build its assertion on this array: `[["a@example.test", "invoice ready"]]`. A recording fake measures not what a call **does** but **whom it told what**. The notifier in production would reach out to the network; the fake notifier does not, so the test is both fast and independent of the state of the network.

```ruby
# canned.rb — a canned response
exchange = Object.new
exchange.define_singleton_method(:rate) { |_currency| 42.5 }

p exchange.rate(:usd)
p exchange.singleton_methods
```
```
42.5
[:rate]
```

The canned response did no computation: whatever was asked, `42.5` was returned. The object has a single call and that call is the one the test wrote. This cuts a test's dependency on an external source, for example an exchange rate service; but it also raises the question of how well the fake represents the real one. The fake notifier took two arguments; the real notifier may ask for a third argument, the channel.

```ruby
# drift.rb — a fake object can really drift apart from the real one
class FakeNotifier
  attr_reader :sent

  def initialize = @sent = []

  def deliver(recipient, text) = @sent << [recipient, text]
end

class RealNotifier
  def deliver(recipient, text, channel) = [recipient, text, channel]
end

fake = FakeNotifier.new
fake.deliver("a@example.test", "invoice ready")
p fake.sent

begin
  RealNotifier.new.deliver("a@example.test", "invoice ready")
rescue ArgumentError => e
  p e.class
end
```
```
[["a@example.test", "invoice ready"]]
ArgumentError
```

The call written with the fake object passed; the same call raised `ArgumentError` on the real object. The test was green and production broke. **PG21.** A fake object imitates the behavior of the real one; it does not verify it. To catch the drift between the fake and the real one, either a separate test run with the real object or an assertion that checks that the fake follows the real one's signature is needed. This measurement shows only that a drift can exist; it does not measure a tool that catches the drift automatically.

The expectation object adds a decision to the recording object. It keeps what was called, but at the end of the test it turns this into an assertion: "exactly one notification should have gone to this recipient".

```ruby
# expectation.rb — an expectation object
class NotificationExpectation
  def initialize(recipient, expected_count)
    @recipient = recipient
    @expected_count = expected_count
    @calls = []
  end

  def deliver(recipient, _text) = @calls << recipient

  def verify = @calls.count(@recipient) == @expected_count
end

expectation = NotificationExpectation.new("a@example.test", 1)
expectation.deliver("a@example.test", "invoice ready")
p expectation.verify
expectation.deliver("a@example.test", "invoice ready")
p expectation.verify
```
```
true
false
```

After the first call the expectation holds; after the second call it does not: two notifications have gone to the same recipient. The distinction is this: the recording object collects the information, and the expectation object ties a decision to that information. If the decision is set up wrongly, the test verifies the wrong thing; the expected count is the test's own assumption.

## Test Data

Test data is the records produced as the input of a test: a customer, an invoice, a user. Writing the data by hand repeats the same lines in every test; a **test data generator** produces records by a rule. The simplest generator keeps a counter.

```ruby
# generator.rb — a counter-based test data generator
def customer_generator
  counter = 0
  -> { counter += 1; "customer#{counter}" }
end

generate = customer_generator
p generate.call
p generate.call
p customer_generator.call
```
```
"customer1"
"customer2"
"customer1"
```

Two calls from the same generator gave `"customer1"` and `"customer2"`: the counter rises by one on every call. A new generator gave `"customer1"` again. These two observations are the answer to isolation. If one generator is shared among all tests, a record produced by one test advances the counter of the other, and the second test sees `"customer2"` instead of `"customer1"`. When each test builds its own generator, the shared state is divided. This is the same as the copy of the settings table in the previous lesson, for a counter. **PG22.** The measurement shows a counter-based generator; for a generator that produces a persistent record in a database to provide the same isolation, a separate cleanup step is needed, and it was not measured here.

## The Bounding Measurement

The result of mocking looks as if it solved everything: the clock is fixed, the notifier is fake, the counter is isolated. But removing one determinant does not remove another. If test data is produced with a seed, the seed determines the result.

```ruby
# data.rb — seeded test data
require_relative "determinant"

p Array.new(3) { Random.new(1).rand(100) }.uniq.size
p (r = Random.new(1); Array.new(3) { r.rand(100) })
p (r = Random.new(2); Array.new(3) { r.rand(100) })

data = lambda do |run|
  r = Random.new(run.seed)
  Array.new(3) { r.rand(100) }
end
p determinant(data)

fixed = lambda do |_run|
  r = Random.new(7)
  Array.new(3) { r.rand(100) }
end
p determinant(fixed)
```
```
1
[37, 12, 72]
[40, 15, 72]
["order", "D- O+ E-", [37, 12, 72]]
["code", "D- O- E-", [47, 68, 25]]
```

The first line shows that if a new `Random.new(1)` is built on every call, the three values are always the same: a single value. Seed 1 and seed 2 gave two different triples. When `determinant` takes the seed from the run, the result is `order`: the evidence is `D- O+ E-`, and the base is the three values of seed 1. When the seed is a fixed number, that is `Random.new(7)`, the result is `code`: the computation does not look at the run's seed. **Mocking removes one determinant; it does not remove another by itself.** Fixing the clock removed the environment; a data generator that takes its seed from the run left the order determinant. Which determinant remains is understood not from whether something is fake but from what the result continues to look at. **PG23.** The seeded data here consists of three numbers; why a test sometimes fails when the records of a real data generator depend on the seed is the same reason as in the previous lesson and was not measured separately.

The practical consequence of this distinction is as follows: to make a test stable, one must first measure which determinant moves the result. Faking the clock does not rescue a test whose result depends on the order; fixing the order does not rescue a test that depends on the clock. The seed table of the previous lesson and the clock measurement of this lesson ask the same question on two different variables, and the answer each time depends on the variable itself.

## Summary

- A test that reads the clock depends on what the environment says: in the `clock-reading test` row the evidence is `D- O- E+`, and the result changes in March 2027.
- The determinant of this lesson is **environment**. When the clock is passed in as a parameter and a fixed clock source is used, the result becomes `code`.
- A recording fake collects what was told, a canned response returns a ready answer and an expectation object ties a decision to the call.
- A counter-based test data generator gives a different value on each call; a generator of its own for each test divides the shared state.
- Mocking removes one determinant and does not remove another by itself: test data produced from the run's seed stays as `order`.

## Next Step

The first half of this course measured what determines the result for packages and tests: the package specification for the file name, the lock file for the installed version, the order for the failing test and the environment for the test that reads the clock. The second half moves to code running in production. The lesson The Concurrency Model measures that the same code gives a different result by seed when it runs with a counter without a lock, and shows why a mutex removes this difference.
