With minitest-mock, how does Object#stub replace Time.now for one block in a subscription-renewal test, and what restores the original?
answer
- stub(name, value_or_callable) { }
- singleton class alias and define_method
- ensure puts the original back
- method must already exist: NameError
- callables are called with the arguments
basics
~20 sTime.stub(:now, fixed_time) { ... } redefines now on Time's singleton class for the length of the block, returning fixed_time or calling it if it is callable; an ensure clause restores the original method even if the block fails.
solid answer
~40 s`Object#stub(name, val_or_callable, *block_args) { ... }` comes from minitest-mock. It aliases the original method on the receiver's **singleton class**, defines a replacement that returns `val_or_callable` (or **calls** it with the arguments when it responds to `call`), yields the receiver to your block, and in an **`ensure`** puts the original back, so the stub disappears when the block ends even after a failed assertion. `Time.stub(:now, Time.new(2026, 10, 1)) { renewal.due? }` therefore affects every `Time.now` call made during the block, anywhere, because it replaces a class method. The method must already exist, or stubbing raises `NameError`. Stubbing an instance method changes only that one object, not every instance of its class.
code
ruby · 16 linesrequire "minitest/autorun"
require "minitest/mock"
class SubscriptionTest < Minitest::Test
def test_grace_period_ends_after_three_days
sub = Subscription.new(renews_at: Time.new(2026, 10, 1))
Time.stub(:now, Time.new(2026, 10, 3)) do
refute_predicate sub, :lapsed?
end
Time.stub(:now, Time.new(2026, 10, 5)) do
assert_predicate sub, :lapsed?
end
end
endgo deeper
Recall the shape Time.stub(:now, fixed_time) { ... } and that the replacement lasts only for the block.
Explain the singleton-class alias, the ensure restoration, callable values, and why stubbing an instance method affects only one object.
Judge when a process-wide class-method stub is acceptable and when to inject a clock instead, and watch for stubs that accept calls the real method rejects.
Set a team convention for time in tests, such as an injected clock by default and Time.stub only at hard boundaries, and explain what it saves.
## What `stub` is `Object#stub` is the stubbing half of minitest-mock (`require "minitest/mock"`). It is defined on `Object`, so any object, including a class such as `Time`, can have one method temporarily replaced: ```ruby def test_subscription_is_due_on_its_renewal_date subscription = Subscription.new(renews_at: Time.new(2026, 10, 1)) Time.stub(:now, Time.new(2026, 10, 1, 9, 0, 0)) do assert_predicate subscription, :due? end end ``` ## How it works, step by step The implementation is about forty lines: 1. It takes the receiver's **singleton class** (for `Time`, the class that holds `Time.now`). 2. It aliases the current `now` to a hidden name, `__minitest_stub__now`. 3. It defines a new `now` on the singleton class that ignores the original and returns your value. 4. It calls your block, passing the receiver. 5. In an **`ensure`** it removes the replacement, aliases the hidden method back to `now`, and deletes the hidden name. Because step 5 is in `ensure`, a failed assertion or an exception inside the block still restores `Time.now`. The stub cannot leak into the next test through the normal failure paths. ## Values versus callables The second argument is interpreted in one of two ways: - If it **does not respond to `call`**, it is returned as is on every call: all `Time.now` calls in the block see the same instant. - If it **responds to `call`** (a lambda, a proc, a `Method`), it is called with the arguments the stubbed method received, and its result is returned. That gives argument-dependent answers or a sequence: ```ruby times = [Time.new(2026, 10, 1), Time.new(2026, 10, 2)] Time.stub(:now, -> { times.shift }) do # first call gets Oct 1, second Oct 2 end ``` The flip side: to make a stubbed method **return** a lambda, wrap it in another callable, or `stub` will call it instead of returning it. Extra arguments after the value (`*block_args`) are yielded to a block that the calling code passes to the stubbed method. ## Scope: which calls are affected | stub target | who sees the replacement | |---|---| | `Time.stub(:now, t)` | every `Time.now` call in the process during the block | | `subscription.stub(:expired?, true)` | that one `subscription` object only | | `Subscription.stub(:find, sub)` | calls to the class method `Subscription.find` | Instance methods can only be stubbed per object: minitest-mock has no "any instance of this class" form, so the usual route is to stub the class method that builds the object (`Subscription.stub(:new, fake)`) or to inject the object. ## Common mistakes - Expecting `Time.stub(:now, t)` to be undone by `teardown`: it is undone when its own block ends, so assertions that rely on the frozen time must sit inside the block. - Evaluating the clock outside the block, for example building `expected = Time.now + 86_400` before entering it, and then comparing with a value computed inside. - Stubbing a method on a freshly built object while the code under test builds its own instance: the stub sits on the wrong object and never fires. - Passing a `Method` or lambda meant as a return value, which `stub` calls instead of returning. ## Preconditions and errors - **The method must exist.** Stubbing `Time.stub(:today, t)` raises `NameError`, because there is nothing to alias. Define a singleton method first if you really need a new one. - **Signatures are not checked.** Keyword arguments to a callable are not compared with the real method's, so a stub can accept calls the real method would reject. - **Nesting works**: stub two methods by nesting two `stub` blocks; each one restores its own method on the way out. ## When `Time.stub(:now)` is the right tool It is quick for code that reads the clock directly through `Time.now` and that you cannot easily change. For code you own, passing a clock in (`clock: Time` as a default keyword) makes the dependency visible and avoids a process-wide replacement.
- With minitest-mock, what happens to Time.now if an assertion fails inside Time.stub(:now, t) { ... }?The assertion raises `Minitest::Assertion`, which leaves the block, and `stub`'s `ensure` clause runs on the way out: it removes the replacement and aliases the original `now` back. The next test sees the real clock.
- With minitest-mock, how do you make a stubbed method return a different value on each call?Pass a callable as the value. `stub` calls anything that responds to `call` with the method's arguments, so `Time.stub(:now, -> { times.shift })` returns the next element each time. A plain value is returned unchanged on every call.
A stub is a sticky note placed over one line of a printed timetable while you are in the room: everyone who reads that line meanwhile sees the note, and the ensure clause peels it off as you leave, whatever happened inside.
saying these in an interview costs you the question
- Object#stub replaces the method on every instance of the class.
- The stub stays in place until teardown runs.
- You can stub a method the object does not define yet.
- Passing a lambda as the value makes the stubbed method return that lambda.
- A failing assertion inside the block leaves Time.now stubbed.