skip to content

Minitest

Ruby's lightweight test framework: assert-style Minitest::Test, a spec layer on top, mocks in a separate gem since 6.0, and seeded random order. The minimal counterweight to RSpec.

on this pageshow

explore

questions

14

With Minitest, what makes a method in a Minitest::Test subclass run as a test, and when do setup and teardown run?

level: juniorimportance: must knowfreq 72%

answer

  1. subclass Minitest::Test
  2. public methods matching /^test_/
  3. one new instance per test
  4. setup before, teardown after, every test
  5. teardown runs after a failure too

basics

~10 s

Minitest runs every public method whose name starts with test_ in a Minitest::Test subclass, each on a fresh instance. setup runs before every test and teardown after it, even when the test failed.

solid answer

~40 s

You `require "minitest/autorun"`, subclass `Minitest::Test` and write public methods whose names start with `test_`; Minitest collects them with `public_instance_methods(true).grep(/^test_/)` and, by default, runs them in random order. Every test method runs on a **new instance** of the class, so the instance variables `setup` assigns are rebuilt for each test and never leak into the next one. The per-test lifecycle is `setup`, the test method, then `teardown`, and `teardown` still runs when an assertion failed or the code raised, so it is where you close files or restore global state. A failed assertion raises `Minitest::Assertion` and is reported as a Failure (`F`), any other exception as an Error (`E`), and `skip` as `S`.

code

ruby · 21 lines
ruby
require "minitest/autorun"

class FineCalculatorTest < Minitest::Test
  def setup
    @calc = FineCalculator.new(daily_rate_cents: 25, cap_cents: 1_000)
    @log = File.open("fines.log", "w")
  end

  def teardown
    @log.close
    File.delete("fines.log")
  end

  def test_no_fine_when_on_time
    assert_equal 0, @calc.fine_for(0)
  end

  def caps_the_fine_test          # never runs: no test_ prefix
    assert_equal 1_000, @calc.fine_for(90)
  end
end

go deeper

for a junior

Recall the three rules: subclass Minitest::Test, prefix methods with test_, and use setup and teardown for per-test preparation and cleanup.

for a middle

Explain that each test method gets a new instance, that teardown runs in its own wrapper after failures, and how F, E and S differ in the report.

for a senior

Show you can spot state that escapes the per-instance model, such as constants, class variables, globals and ENV, and put its restoration in teardown.

for a principal

Frame the per-test instance and random order as deliberate isolation defaults, and judge when a shared base class or module helps a suite versus hiding setup.

## The shape of a Minitest test file Minitest is the testing library Ruby bundles (Ruby 4.0.7 ships Minitest 6.0.0 as a bundled gem). Its classic style, `minitest/test`, is plain Ruby classes and methods: ```ruby require "minitest/autorun" require_relative "../lib/fine_calculator" class FineCalculatorTest < Minitest::Test def setup @calc = FineCalculator.new(daily_rate_cents: 25, cap_cents: 1_000) end def test_charges_the_daily_rate assert_equal 75, @calc.fine_for(3) end def test_caps_the_fine assert_equal 1_000, @calc.fine_for(90) end end ``` `require "minitest/autorun"` loads Minitest (both the test and the spec layer) and registers an `at_exit` hook, so running `ruby -Ilib test/fine_calculator_test.rb` executes every test class that was loaded. ## What counts as a test - The class must inherit from **`Minitest::Test`** (directly or through your own base class). - A test is any **public** instance method whose name matches `/^test_/`. Minitest finds them with `public_instance_methods(true)`, so `test_` methods inherited from a superclass or an included module run too, and a `private` `test_` method does not. - Helper methods without the prefix (`def build_member`) are ordinary methods you call from tests. - There are no annotations or registration calls; the name is the whole contract. By default the methods of a class run in **random order**, seeded so that a failing order can be replayed. That default exists to expose tests that secretly depend on each other. ## One instance per test, and the lifecycle around it For each test method Minitest creates a **new instance** of the class, passing the method name, and calls `run` on it. Inside `run` the order is fixed: 1. `before_setup`, `setup`, `after_setup` - the middle one is yours; the outer two are hooks for plugin authors. 2. The test method itself. 3. `before_teardown`, `teardown`, `after_teardown` - each wrapped separately, so all of them run even when the test body, or `setup`, raised. Because the instance is new every time, `@calc` assigned in `setup` is a fresh object for each test: if `test_caps_the_fine` mutated it, `test_charges_the_daily_rate` would never see the change. Anything stored in a constant, a class variable or a global, however, is shared across tests, and `setup` does not reset it. | hook | runs | typical job | |---|---|---| | `setup` | before every test | build the object under test, open a temp dir | | `teardown` | after every test, pass or fail | close files, restore `ENV`, delete temp data | | `before_setup` / `after_teardown` | around the above | reserved for libraries extending Minitest | If you mix a module with its own `setup` into several test classes, call `super` inside it so the chain still reaches every definition. ## How outcomes are reported An assertion that fails raises **`Minitest::Assertion`**, which stops that test method at that line. Note that `Minitest::Assertion` inherits from `Exception`, not `StandardError`, so a bare `rescue => e` in code under test does not swallow a failed assertion. Minitest's wrapper then records the result: - `.` - the test passed. - `F` - **Failure**: an assertion did not hold. - `E` - **Error**: some other exception escaped (a `NoMethodError`, an `ArgumentError` you did not expect). It is wrapped in `Minitest::UnexpectedError`. - `S` - **Skipped**: the test called `skip "reason"`, which raises `Minitest::Skip`. The summary line reads `N runs, N assertions, N failures, N errors, N skips`. The Failure/Error split matters when you read a red build: an `F` means the code returned the wrong answer, an `E` usually means the test or the code crashed before it could answer. ## Common first-week mistakes - Naming a method `caps_the_fine_test` or `check_cap`: it never runs, and the suite stays green without testing anything. - Putting shared state in `@@count` or a constant and expecting `setup` to reset it. - Doing cleanup at the end of the test body instead of in `teardown`, so a failing assertion skips the cleanup and pollutes later tests. - Overriding `setup` in a subclass without calling `super` when the parent class also builds fixtures.

  • In Minitest, setup assigns @calc and test_a calls @calc.reset_rate!(0). Does test_b see the zero rate?
    No. Minitest builds a new instance of the test class for every test method, and `setup` runs again on that instance, so `test_b` gets a freshly constructed `@calc`. The change would leak only if the state lived somewhere shared, such as a constant, a class variable or a global.
  • In Minitest, what is the difference between a Failure and an Error in the run summary?
    A Failure (`F`) is a `Minitest::Assertion` raised by an assertion that did not hold: the code answered wrongly. An Error (`E`) is any other exception escaping the test, wrapped in `Minitest::UnexpectedError`: the code or the test crashed. Both turn the run red, but they point at different fixes.
  • With Minitest, why should cleanup live in teardown rather than at the end of the test method?
    A failing assertion raises and stops the test method at that line, so cleanup written after it never runs. `teardown` is called by the test's `run` method in its own exception wrapper after the body, whether the body passed, failed, errored or skipped, so files, `ENV` changes and temp data are always restored.

Each test is an exam candidate handed a brand-new answer sheet: setup fills in the header, the candidate writes, teardown collects the sheet whether the answers were right or wrong, and nobody inherits the previous candidate's scribbles.

saying these in an interview costs you the question

  • Any method in the test class runs as a test, whatever its name.
  • setup runs once per class, so tests share one @calc object.
  • teardown is skipped when an assertion in the test fails.
  • Tests run top to bottom in the order they are written in the file.
  • A failed assertion is reported the same way as an unexpected exception.
open as a page

With Minitest 6, how do you run one test file, one test method, or only the tests matching a pattern?

level: juniorimportance: must knowfreq 58%

basics

~20 s

Run the file with ruby -Ilib:test, and filter tests with --include (alias -n) using a name or a /regexp/, or --exclude. Under rake use N=, X= or A=; Minitest 6's minitest command also accepts file.rb:line.

open as a page

With Minitest, how does assert_raises decide whether a test passes, what does it return, and how do you check the error message?

level: middleimportance: must knowfreq 55%

basics

~10 s

assert_raises(ErrorClass) { code } passes when the block raises that class or a subclass, and returns the exception so you can assert on its message. No exception, or a different one, fails the test.

open as a page

With minitest-mock, how do Minitest::Mock#expect and #verify work, and what happens when code calls the mock differently than expected?

level: middleimportance: must knowfreq 52%

basics

~20 s

expect(name, retval, args) records one expected call and its return value; each actual call consumes one expectation. verify raises MockExpectationError for any expected call that never happened; unexpected arguments raise it at call time, unknown methods raise NoMethodError.

open as a page

With minitest-mock, how does Object#stub replace Time.now for one block in a subscription-renewal test, and what restores the original?

level: middleimportance: must knowfreq 50%

basics

~20 s

Time.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.

open as a page

With Minitest, why do tests run in random order, and how do you replay a failing order with --seed and shrink it with --bisect?

level: middleimportance: must knowfreq 50%

basics

~20 s

Minitest shuffles classes and methods with a printed seed to expose tests that depend on each other. Rerun with --seed N to replay the order, then minitest --bisect with that seed to find the smallest failing set.

open as a page

With Minitest, why is assert_equal clearer than assert(a == b), and when do assert_nil, assert_predicate or assert_in_delta fail more clearly?

level: juniorimportance: should knowfreq 58%

basics

~20 s

A bare assert only knows it got false; assert_equal expected, actual prints both values or a diff. Minitest 6 rejects a nil expected value, so use assert_nil, and assert_predicate, assert_includes and assert_in_delta name what was wrong.

open as a page

With Minitest 6 on Ruby 4.0, why does require "minitest/mock" raise LoadError, and how do you get Minitest::Mock and Object#stub back?

level: juniorimportance: should knowfreq 35%

basics

~10 s

Minitest 6.0 removed minitest/mock.rb and moved it into the separate minitest-mock gem, which Ruby 4.0 does not bundle. Add minitest-mock to the test dependencies; the require path, Minitest::Mock and Object#stub stay the same.

open as a page

In Minitest::Spec, what do describe, it, let and before turn into, and why must expectations be written as _(value).must_equal?

level: middleimportance: should knowfreq 42%

basics

~20 s

describe creates a subclass of Minitest::Spec, which is a Minitest::Test; it defines test_ methods, before and after become setup and teardown, and let is a lazy per-test memoised method. Minitest 6 expectations work only on a wrapper: _(x), value(x) or expect(x).

open as a page

In Minitest 6, how are plugins and reporters loaded, and why does a suite's --pride flag or custom reporter stop working after the upgrade?

level: middleimportance: should knowfreq 30%

basics

~10 s

Minitest 6 no longer auto-loads every installed minitest/*_plugin.rb. Require the plugin in the test helper, call Minitest.load :name, or call Minitest.load_plugins; reporters are added to Minitest.reporter from a plugin's init hook.

open as a page

When a library-fine calculator's suite moves from Minitest 5 to Minitest 6, which assertion and spec changes break tests, and how do you fix each?

level: seniorimportance: should knowfreq 28%

basics

~10 s

Minitest 6 turned 5.x deprecations into failures: bare obj.must_equal needs _(obj), assert_equal nil needs assert_nil, assert_send is gone, and the MiniTest namespace no longer exists. Fix the warnings on 5.27 first, then upgrade.

open as a page

In a Minitest subscription-renewal suite, why can stubbing Time.now or rand still give wrong or flaky results, and when is an injected hand-written fake better?

level: seniorimportance: should knowfreq 32%

basics

~20 s

Time.stub(:now) misses clocks that do not go through Time.now, and Kernel.stub(:rand) misses rand called on self; both stubs are process-wide and signature-blind. Injecting a clock and a Random, with a small hand-written fake, removes those gaps.

open as a page

To speed up a 20-minute Minitest suite with parallelize_me!, what does it actually parallelise, how does MT_CPU size it, and which tests break?

level: seniorimportance: should knowfreq 35%

basics

~20 s

parallelize_me! runs that class's tests on a pool of threads in the same process, sized by MT_CPU or the CPU count. Under CRuby's GVL it mainly speeds up tests that wait on I/O, and it breaks tests that share process-wide state.

open as a page

With Minitest::Benchmark, what does assert_performance_linear 0.99 check, and why is it not a time limit for a slow test?

level: middleimportance: nice to knowfreq 14%

basics

~20 s

It times the block at each size in bench_range and passes when those times fit a straight line with a coefficient of determination of at least 0.99. It checks how cost grows with input size, not how many seconds anything takes.

open as a page