Mocking, Stubbing & Test Doubles
Test doubles help isolate units, simulate external systems, and verify collaborations without hitting real services. Ruby offers flexible options that work across RSpec and Minitest.
Catalog of doubles
Mocks
Expect calls ahead of time (`expect(service).to receive(:call)`). Use sparingly; prefer spies to avoid overspecifying behaviour.
Stubs
Override method return values during the test (`allow(user).to receive(:timezone).and_return('UTC')`). Restore the original behaviour automatically after each example.
Spies
Observe interactions after the fact (`analytics = spy('Analytics'); ...; expect(analytics).to have_received(:track)`), great for verifying messaging behaviour.
RSpec syntax for doubles
Minitest stubs & mocks
`Minitest::Mock#expect` verifies arguments and call counts; remember to call `verify`. Use `stub` to patch methods temporarily.
External calls & HTTP mocking
- WebMock: intercept HTTP requests (`stub_request(:post, 'https://api.example.com')`). Fail the spec if unexpected calls leak through.
- VCR: record and replay HTTP responses. Store cassettes in `spec/fixtures/vcr_cassettes` or `test/support/cassettes`.
- Fakes: build lightweight in-memory implementations (e.g., `FakePaymentGateway`) for fast tests while preserving behaviours relevant to the suite.
Avoid these pitfalls
- Over-mocking locks tests to implementation. Mock only boundaries, not private methods or Ruby core APIs.
- Stubbing time globally? Prefer `travel_to` (ActiveSupport) or `freeze_time` to keep tests deterministic.
- Remember to clean state between examples—WebMock + VCR cassettes should reset so tests don’t leak data.