Lesson 20 / 25

Testing with RSpec and Minitest

Write tests with RSpec and Minitest, including doubles.

A strong testing culture

Ruby has a strong testing culture, and two frameworks dominate. Minitest ships with Ruby and is used by Rails by default: test classes subclass Minitest::Test, methods start with test_, and assertions such as assert_equal(expected, actual) and assert_raises check behaviour; it also offers a spec style. RSpec provides a readable DSL: describe and context group examples, it defines them, expect(actual).to eq(expected) asserts with matchers (be_truthy, include, raise_error, change { ... }.by(1), have_attributes), and let defines lazily evaluated helpers. Both support test doubles: RSpec's instance_double(PaymentGateway, charge: "pay_1") creates verified doubles that fail if you stub methods the real class does not have, and expect(gateway).to receive(:charge).with(...) checks interactions. Use FactoryBot for test data, WebMock or VCR to stub HTTP, and SimpleCov for coverage. Run tests on every change in CI, and keep them fast by isolating I/O.

The same behaviour tested with RSpec and Minitest

Verified doubles catch stubs of methods that do not exist.

# spec/checkout_spec.rb (RSpec)
RSpec.describe Checkout do
  let(:gateway) { instance_double(PaymentGateway, charge: "pay_1") }
  subject(:checkout) { described_class.new(gateway) }

  it "charges the order total once" do
    expect(checkout.pay(order_id: "o-1", amount_paise: 49_900)).to eq("pay_1")
    expect(gateway).to have_received(:charge).with("o-1", 49_900).once
  end

  it "rejects non-positive amounts" do
    expect { checkout.pay(order_id: "o-2", amount_paise: 0) }.to raise_error(ArgumentError)
  end
end

# test/checkout_test.rb (Minitest)
require "minitest/autorun"

class CheckoutTest < Minitest::Test
  FakeGateway = Struct.new(:calls) do
    def charge(order_id, amount) = (calls << [order_id, amount]; "pay_1")
  end

  def test_charges_once
    gateway = FakeGateway.new([])
    assert_equal "pay_1", Checkout.new(gateway).pay(order_id: "o-1", amount_paise: 49_900)
    assert_equal [["o-1", 49_900]], gateway.calls
  end
end

Two languages, one meaning

RSpec and Minitest are like writing the same rule in two styles: one as a sentence ("it charges the order once"), the other as a plain checklist. Both prove the same thing; teams choose the style they read best.

Quick check: What does RSpec's instance_double provide over a plain double?

  • Faster tests
  • Database access
  • Automatic test data
  • Verification that stubbed methods actually exist on the real class with matching arguments
Answer

Verification that stubbed methods actually exist on the real class with matching arguments — Verified doubles fail when you stub methods the real class does not define.