RSpec Rails: Factory Bot, VCR en de Testpatronen die Echt Schalen
RSpec Rails goed opgezet — Factory Bot associaties, VCR voor externe APIs, gedeelde voorbeelden en CI-trucs die je Rails testsuite onder 3 minuten houden.
De eerste keer dat ik als fractional CTO een codebase aantrof zonder enige testsuite dacht ik dat het uitzonderlijk was. Na de vijfde keer had ik een protocol. De eerste week verloopt altijd hetzelfde: kijk wat er aan tests bestaat, voer ze uit, meet hoe lang ze duren, en bekijk hoeveel tests daadwerkelijk bugs vangen versus simpelweg controleren of een factory een record aanmaakt. Negen van de tien codebases vallen in dezelfde twee categorieën: bijna geen tests, of een opgeblazen suite die vijfentwintig minuten duurt op CI en bij elke derde run flaky is. Geen van beide is nuttig. Beide zijn duur.
Na negentien jaar Rails ben ik uitgekomen op een RSpec Rails-opzet die schaalt van een startup met tien ontwikkelaars naar een engineeringteam van veertig, zonder dat de suite een aansprakelijkheid wordt. Dit artikel is die opzet, op papier.
Waarom Ik Standaard Kies voor RSpec Rails boven Minitest
Ik wil dit vooraf duidelijk maken, want het is een echte discussie en ik heb een mening. Minitest wordt meegeleverd met Rails, draait sneller out-of-the-box en is de juiste standaard voor eenvoudige projecten. Ik behandelde Rails Minitest met fixtures precies om die reden. Maar voor teams die rijke testinfrastructuur nodig hebben — gedeelde voorbeelden over meerdere modeltypes, geneste context-blokken, custom matchers voor domeinassertaties en leesbare spec-uitvoer op CI — is RSpec de extra overhead waard.
Die overhead is reëel. RSpec Rails laadt een DSL, een extensielaag en een formatter bij elke run. Op een volwassen codebase met vijfduizend specs verdwijnt die overhead in het ruis. Op een project met vijftig specs kan het je doorlooptijd verdubbelen. Gebruik Minitest voor nieuwe soloproject. Gebruik RSpec voor teams die iets bouwen dat serieuze langetermijntestdekking nodig heeft.
RSpec Rails op de Goede Manier Opzetten
De basis is rechtdoorze. Wat telt is wat je daarna configureert.
# Gemfile
group :development, :test do
gem "rspec-rails", "~> 7.0"
gem "factory_bot_rails", "~> 6.4"
gem "faker", "~> 3.4"
gem "vcr", "~> 6.3"
gem "webmock", "~> 3.23"
end
group :test do
gem "shoulda-matchers", "~> 6.2"
gem "simplecov", require: false
end
bundle install
bin/rails generate rspec:install
De generator maakt .rspec, spec/spec_helper.rb en spec/rails_helper.rb. De splitsing is bewust. spec_helper.rb is voor pure Ruby-specs die Rails niet nodig hebben. rails_helper.rb laadt Rails en is voor alles wat de database, routes of views raakt.
De configuratie die ik in elk project gebruik:
# spec/rails_helper.rb
require "spec_helper"
require "simplecov"
SimpleCov.start "rails"
ENV["RAILS_ENV"] ||= "test"
require_relative "../config/environment"
require "rspec/rails"
require "factory_bot_rails"
require "shoulda/matchers"
Dir[Rails.root.join("spec/support/**/*.rb")].sort.each { |f| require f }
RSpec.configure do |config|
config.use_transactional_fixtures = true
config.infer_spec_type_from_file_location!
config.filter_rails_from_backtrace!
config.include FactoryBot::Syntax::Methods
end
Shoulda::Matchers.configure do |config|
config.integrate { |with| with.test_framework(:rspec).and.library(:rails) }
end
Twee zaken verdienen aandacht: use_transactional_fixtures = true wikkelt elke spec in een databasetransactie die aan het einde wordt teruggedraaid — dit is de grootste enkelvoudige prestatiewinst in elke RSpec Rails-opzet. En Dir[...] laadt automatisch alles in spec/support/, waar je gedeelde contexten, custom matchers en VCR-configuratie leven.
Factory Bot: Patronen die Niet Ontploffen
Factory Bot is uitstekend. Het is ook de meest voorkomende oorzaak van trage, fragiele testsuites in elke Rails-codebase die ik heb geërfd. De fout die iedereen maakt: factories bouwen die standaard te veel data aanmaken.
# spec/factories/users.rb — NIET zo doen
FactoryBot.define do
factory :user do
name { Faker::Name.name }
email { Faker::Internet.email }
association :subscription # maakt een Subscription-record aan
association :company # maakt een Company-record aan
after(:create) { |u| u.generate_api_token! }
end
end
Elke create(:user) vuurt drie INSERT-statements af en voert een callback uit. Als een spec vijf gebruikers nodig heeft en alleen hun e-mailadressen gebruikt, heeft hij vijftien records aangemaakt die hij nooit aanraakt.
De juiste aanpak: een minimale factory met expliciete traits.
# spec/factories/users.rb — ZO doen
FactoryBot.define do
factory :user do
name { Faker::Name.name }
email { Faker::Internet.unique.email }
password { "password123" }
role { :member }
trait :admin do
role { :admin }
end
trait :subscribed do
after(:create) do |user|
create(:subscription, user: user, plan: :pro)
end
end
trait :with_company do
association :company
end
trait :with_api_token do
after(:create) { |u| u.generate_api_token! }
end
end
end
Nu is create(:user) één enkele INSERT. Wanneer een spec een geabonneerde admin met een API-token nodig heeft, schrijft hij create(:user, :admin, :subscribed, :with_api_token) en maakt alleen aan wat nodig is. Die discipline — traits voor optionele associaties — is het verschil tussen een suite die twee minuten duurt en een die twintig minuten duurt.
Nog een regel die ik overal afdwing: gebruik nooit create als build of build_stubbed volstaat. build_stubbed is de ondergewaardeerde parel van Factory Bot. Het bouwt een object dat doet alsof het gepersisteerd is — met een nep-id, werkende associatiestubs en geen database-interactie.
# Model-spec die alleen validaties test — nul database-aanroepen
let(:user) { build_stubbed(:user, email: nil) }
it { expect(user).not_to be_valid }
it { expect(user.errors[:email]).to include("can't be blank") }
Op schaal snijdt het stubben van alles wat je niet hoeft te persisteren dertig tot vijftig procent van je suitedoorlooptijd. Ik meet het op elk project nadat ik het heb ingevoerd.
VCR en WebMock: Geen Live HTTP meer in je RSpec Rails Suite
Elke Rails-applicatie integreert uiteindelijk met externe APIs — Stripe, Twilio, OpenAI, Postmark, wat de week ook vraagt. De verleiding is om HTTP-aanroepen handmatig te stubben met WebMock. Dat werkt totdat de externe API een responsveld verandert en je stubs stilzwijgend afwijken van de werkelijkheid. VCR lost dit op: het neemt echte HTTP-interacties op bij de eerste run en speelt ze opnieuw af vanuit cassettes bij volgende runs.
# spec/support/vcr.rb
require "vcr"
require "webmock/rspec"
VCR.configure do |config|
config.cassette_library_dir = "spec/fixtures/vcr_cassettes"
config.hook_into :webmock
config.configure_rspec_metadata!
config.filter_sensitive_data("<STRIPE_SECRET_KEY>") do
ENV.fetch("STRIPE_SECRET_KEY", "sk_test_placeholder")
end
config.filter_sensitive_data("<OPENAI_API_KEY>") do
ENV.fetch("OPENAI_API_KEY", "sk-placeholder")
end
config.default_cassette_options = {
record: :new_episodes,
match_requests_on: [:method, :host, :path]
}
end
Tag vervolgens specs met de cassette die je wilt:
# spec/services/stripe_billing_service_spec.rb
RSpec.describe StripeBillingService, :vcr do
describe "#create_subscription" do
let(:user) { create(:user, :with_stripe_customer) }
let(:plan) { create(:plan, stripe_price_id: "price_H5ggYwtDq8") }
it "maakt een Stripe-abonnement aan en slaat het lokaal op" do
subscription = described_class.create_subscription(user: user, plan: plan)
expect(subscription).to be_persisted
expect(subscription.stripe_subscription_id).to start_with("sub_")
expect(user.reload.subscriptions.count).to eq(1)
end
end
end
De eerste keer dat die spec draait, slaat VCR de echte Stripe-sandbox aan en neemt de respons op. Elke volgende run, ook op CI, speelt af vanuit de cassette. Geen netwerkverkeer, geen rate limits, deterministische uitvoer, en de opgenomen respons blijft dicht genoeg bij de echte API om vormwijzigingen in responses te detecteren wanneer je cassettes vernieuwt.
De filter_sensitive_data-blokken zijn niet optioneel — ze verwijderen API-sleutels uit cassettes voordat ze in je repository terechtkomen. Bekijk altijd cassette-bestanden voor je ze commit. API-responses bevatten regelmatig klant-ID’s, tokens en andere data die nooit in versiebeheer thuis horen.
Voor scenario’s waarbij je specifieke foutpaden moet testen — timeouts, 5xx-responsen, authenticatiefouten — zijn ruwe WebMock-stubs overzichtelijker:
RSpec.describe StripeBillingService do
describe "#create_subscription foutafhandeling" do
before do
stub_request(:post, "https://api.stripe.com/v1/subscriptions")
.to_return(status: 402, body: { error: { code: "card_declined" } }.to_json)
end
it "gooit een PaymentDeclinedError bij kaartweigering" do
expect do
StripeBillingService.create_subscription(user: user, plan: plan)
end.to raise_error(StripeBillingService::PaymentDeclinedError)
end
end
end
Gebruik VCR voor happy-path integratietests. Gebruik ruwe WebMock-stubs voor foutscenario’s. Ze werken goed samen.
Gedeelde Voorbeelden: Spec-logica Eén Keer Schrijven
Elke codebase met meer dan een handvol modellen kent terugkerende patronen: elk resource moet worden geauditeerd, elk openbaar eindpunt vereist authenticatie, elk domeingebeurtenis moet een tijdstempel hebben. Gedeelde voorbeelden laten je die assertaties eenmalig definiëren en overal toepassen.
# spec/support/shared_examples/auditable.rb
RSpec.shared_examples "auditable" do
it "registreert een auditvermelding bij aanmaken" do
expect { subject }.to change(AuditLog, :count).by(1)
end
it "slaat de uitvoerende gebruiker op in het auditlogboek" do
subject
expect(AuditLog.last.user).to eq(current_user)
end
end
# spec/support/shared_examples/timestamped.rb
RSpec.shared_examples "timestamped resource" do
it { is_expected.to respond_to(:created_at, :updated_at) }
it { expect(subject.created_at).to be_a(ActiveSupport::TimeWithZone) }
end
Toegepast over je modelspecs:
RSpec.describe Order, type: :model do
it_behaves_like "auditable"
it_behaves_like "timestamped resource"
end
RSpec.describe Invoice, type: :model do
it_behaves_like "auditable"
it_behaves_like "timestamped resource"
end
Twintig regels gedeelde code, consistent toegepast over zoveel modellen als je hebt. Wanneer het auditschema verandert, herstel je het gedeelde voorbeeld eenmalig. Dit patroon maakt een RSpec Rails-suite een hefboomtool in plaats van een onderhoudsbeslag. Ik gebruik gedeelde voorbeelden ook voor feature flag-bewakingsspecs — elk eindpunt achter een feature flag krijgt it_behaves_like "feature-flagged endpoint" voor consistente gedragstests.
RSpec Rails op CI: Onder de Drie Minuten Komen
Een suite die langer dan drie minuten duurt op CI is een suite die ontwikkelaars lokaal overslaan en waarbij ze wachten tot CI hun fouten vangt. Die feedbackloop is een productiviteitstaks. Het doel is drie minuten wandkloktijd op CI, wat betekent dat je bewust omgaat met elke seconde die je toevoegt.
De winsten, op volgorde van impact:
Gebruik --format progress op CI. De standaard documentatieformatter voegt echte overhead toe door elke specnaam op te maken. Het puntformaat is veel sneller en de uitvoer is nog steeds leesbaar voor het diagnosticeren van fouten.
# .rspec
--require spec_helper
--format progress
--color
Paralleliseer met parallel_tests. De gem verdeelt specs automatisch over CPU-kernen. De meeste CI-runners geven je vier tot acht kernen. Vier kernen snijdt je wandkloktijd met ongeveer 60 procent bij een suite met gelijke belastingsverdeling.
gem "parallel_tests", group: [:development, :test]
# .github/workflows/test.yml
- name: Testdatabaseshards aanmaken
run: bundle exec rake parallel:create parallel:migrate
env:
RAILS_ENV: test
- name: RSpec parallel uitvoeren
run: bundle exec parallel_rspec spec/ -- --format progress
env:
RAILS_ENV: test
De GitHub Actions CI/CD-handleiding behandelt de volledige workflowconfiguratie inclusief het cachen van bundler en de testdatabase-opzet.
Profileer je tien traagste specs. RSpec levert --profile ingebouwd mee. Voer bundle exec rspec --profile 10 uit en je vindt altijd twee of drie specs die vijf tot tien seconden kosten vanwege onnodige database-opzet, ongedekte externe HTTP-aanroepen of factories die associaties bouwen die niemand gebruikt.
Tag trage integratiesspecs en scheid ze. Houd je snelle unit- en modelspecs bij elke push draaien. Zet zwaardere integratietests achter een tag.
# spec/rails_helper.rb
RSpec.configure do |config|
config.when_first_matching_example_defined(:slow) do
config.filter_run_excluding :slow unless ENV["RUN_SLOW"] == "true"
end
end
Tag elke spec die externe services opstart of een volledige requestcyclus doorloopt als :slow. CI draait de snelle suite parallel en de trage suite in een aparte job. Ontwikkelaars krijgen snelle feedback bij elke push.
Veelgestelde Vragen
Wat is het verschil tussen RSpec en Minitest in Rails?
Minitest is het ingebouwde testframework van Rails — sneller op te zetten, lagere overhead en de juiste standaard voor eenvoudigere projecten. RSpec is een DSL-gebaseerd framework met een rijker ecosysteem: gedeelde voorbeelden, custom matchers, context- en describe-nesting, en uitvoer die als specificatieproza leest. Beide testen dezelfde code. RSpec schaalt beter voor grotere teams die complexe gedragsspecificaties helder en consistent willen uitdrukken.
Hoe stel ik Factory Bot in bij RSpec Rails?
Voeg factory_bot_rails toe aan je Gemfile, voer bundle install uit en neem FactoryBot::Syntax::Methods op in je RSpec.configure-blok. Maak factorydefinities aan in spec/factories/. Gebruik traits voor optionele associaties in plaats van ze in de basisfactory te zetten. Geef de voorkeur aan build_stubbed in elke spec die geen echte databasepersistentie nodig heeft — het is aanzienlijk sneller en elimineert een categorie database-afhankelijkheidsbugs in je tests.
Moet ik VCR of WebMock gebruiken voor externe API-tests in Rails?
Gebruik beide samen. WebMock blokkeert al het echte uitgaande HTTP in de testomgeving, wat onbedoelde live API-aanroepen voorkomt. VCR zit bovenop WebMock en speelt opgenomen HTTP-interacties opnieuw af vanuit cassettes. Gebruik VCR voor integratietests tegen echte externe APIs. Gebruik ruwe WebMock-stubs wanneer je specifieke foutcondities moet testen — timeouts, 4xx-responsen, netwerkfouten — die moeilijk betrouwbaar op te nemen zijn met VCR.
Hoe versnl ik een trage RSpec Rails-testsuite?
Voer eerst --profile 10 uit om je tien traagste specs te vinden. Schakel over naar build_stubbed in elke spec die de database niet nodig heeft. Vervang dikke basisfactories door slanke factories met expliciete traits. Schakel use_transactional_fixtures = true in om staat tussen specs terug te draaien in plaats van tabellen af te kappen. Paralleliseer met parallel_tests. Meet dan opnieuw — de profieluitvoer liegt zelden over waar je tijd naartoe gaat.
Hulp nodig om je RSpec Rails-suite van pijnlijk naar betrouwbaar te krijgen? TTB Software bouwt en auditeert Rails-testinfrastructuur al negentien jaar. We kennen de patronen die een suite snel houden naarmate de codebase groeit.
Related Articles
Rack Mini Profiler: Prestatieprofiling voor Rails in Development en Productie
Rack Mini Profiler voor Rails: profileer SQL-queries, partials, geheugen en GC in development en productie. Vind N+1s...
Rails Content Security Policy: CSP-headers, Nonces en Turbo-compatibiliteit
Rails content security policy: configureer CSP-headers, genereer nonces voor Turbo en Stimulus, los schendingen op en...
Rails Event Sourcing: Append-Only Domain Events, Projecties en CQRS in Productie
Rails event sourcing: bouw append-only domain event logs, schrijf projecties en implementeer CQRS-patronen in product...