Testing with Minitest and RSpec

Ruby · Advanced · 14 min

Ruby's dynamic nature makes tests the main safety net, and the community treats them as non-negotiable.

Ruby has two dominant test frameworks.

Minitest ships with Ruby (as a bundled gem) and is what Rails uses by default. Tests are classes inheriting from Minitest::Test, and every method starting with test_ is a test:

  • assert_equal expected, actual, assert, refute, assert_nil, assert_includes, assert_in_delta for floats.
  • assert_raises(KeyError) { ... } returns the exception so you can check its message.
  • setup and teardown run around each test.

Run a file with ruby -Itest test/cart_test.rb, or through Rake or Rails. Minitest also has a spec-style syntax (describe/it) and simple mocks and stubs (Object#stub from minitest/mock).

RSpec is the other big choice, with a descriptive DSL:

RSpec.describe Cart do
  it "sums prices" do
    expect(Cart.new([2, 3]).total).to eq(5)
  end
end

It offers rich matchers (eq, include, raise_error, change { }.by(1)), let for lazily defined fixtures, before hooks, shared examples, and built-in doubles (instance_double(Mailer, deliver: true)), which verify that stubbed methods really exist.

Whichever you pick: test behaviour through public methods, keep each test independent, prefer dependency injection over global stubbing, and run the suite in random order (both do by default) to expose hidden coupling.

Example

# test_cart.rb: run with `ruby test_cart.rb`
require "minitest/autorun"

class Cart
  def initialize(prices) = @prices = prices
  def total = @prices.sum
  def item(i) = @prices.fetch(i)
end

class CartTest < Minitest::Test
  def setup
    @cart = Cart.new([2, 3.5])
  end

  def test_total
    assert_in_delta 5.5, @cart.total
  end

  def test_missing_item_raises
    err = assert_raises(IndexError) { @cart.item(9) }
    assert_match(/index 9/, err.message)
  end
end
# 2 runs, 4 assertions, 0 failures, 0 errors, 0 skips

Remember

  • Minitest is built in: subclass Minitest::Test and write test_ methods
  • RSpec uses describe/it/expect with rich matchers and verified doubles
  • Tests run in random order, so keep them independent

Watch out for

  • Putting the actual value first in assert_equal, which makes failure messages misleading.

Check yourself

  1. In Minitest, what is the argument order of assert_equal?

Sign in to answer, see the explanations, and get this lesson back as a quick review later.

Official docs: github.com/minitest/minitest