RUBY ON RAILS · 16 MIN READ ·

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.

RSpec Rails: Factory Bot, VCR en de Testpatronen die Echt Schalen

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.

#rspec-rails #rails-factory-bot #vcr-rspec-rails #rails-testing-patterns #rspec-shared-examples #rails-test-suite-performance #rspec-rails-2026

Related Articles

Laatste sectie. Bel dan alsjeblieft.

Het is een telefoongesprek. Erger dan dat kan het niet worden.

Geen discovery-deck. Geen 45-minuten "kwalificatiegesprek." 30 minuten, jouw probleem, mijn mening. Als we een fit zijn weet je dat in minuut 12.

Directe lijn — Roger neemt zelf op
+31 6 5123 6132
Ma–vr, 09:00–18:00 CET · Nu beschikbaar

OF
info@ttb.software