Wer mehrere Policy-Objekte testet, kopiert dieselben let-Deklarationen in jede Spec-Datei. Das verletzt das DRY-Prinzip und macht neue Tests zur Pflichtübung statt zum Vergnügen. Shared Context löst genau dieses Problem.
Was testen wir?
Angenommen, wir schreiben Policy-Objekte wie dieses:
# lib/policies/user_settings_policy.rb
class UserSettingsPolicy
def can_show?
user.admin? || user.id == resource.id
end
private
attr_reader :user, :resource
def initialize(user, resource)
@user = user
@resource = resource
end
end
Wir haben User- und UserSettings-Klassen, deren Instanzen durch user_id verbunden sind. Um das Policy-Objekt zu testen, brauchen wir einen Admin-Benutzer, einen normalen Benutzer und eine Ressource.
# spec/policies/user_settings_policy_spec.rb
RSpec.describe UserSettingsPolicy do
let(:user) do
Models::User.new(
id: SecureRandom.uuid,
first_name: 'Nick',
last_name: 'Owen',
email: 'nick.owen@exam.com'
)
end
let(:admin) do
Models::User.new(
id: SecureRandom.uuid,
first_name: 'Jack',
last_name: 'Jones',
email: 'jones@exam.com',
role: :admin
)
end
let (:resource) { Models::UserSettings.new(user_id: user.id) }
describe '#can_show?' do
context 'when a user has an admin role' do
subject { described_class.new(admin, resource) }
it 'returns true' do
expect(subject.can_show?).to be_truthy
end
end
context 'when a user has a user role' do
subject { described_class.new(user, resource) }
...
end
end
end
Diese let-Deklarationen nehmen viel Platz ein. Und beim nächsten Policy-Objekt müssen wir Benutzer und Admin erneut deklarieren. Das ist nicht der richtige Weg.
Wie löst Shared Context das Duplikationsproblem?
Erstellen Sie ein shared-Verzeichnis innerhalb von spec und darin eine Datei users.rb:
# spec/shared/users.rb
RSpec.shared_context :users do
let(:user_id) { SecureRandom.uuid }
let(:user) do
Models::User.new(
id: user_id,
first_name: 'Nick',
last_name: 'Owen',
email: 'nick.owen@exam.com'
)
end
let(:admin_id) { SecureRandom.uuid }
let(:admin) do
Models::User.new(
id: admin_id,
first_name: 'Jack',
last_name: 'Jones',
email: 'jones@exam.com',
role: :admin
)
end
end
Die IDs sind in separate Variablen extrahiert, damit wir nicht jedes Mal user.id aufrufen müssen. Jetzt den Shared Context in spec_helper.rb laden:
Dir[File.expand_path('shared/**/*.rb', __dir__)].each { |f| require f }
Wie sieht die aufgeräumte Spec-Datei aus?
# spec/policies/user_settings_policy_spec.rb
RSpec.describe UserSettingsPolicy do
include_context :users
let (:resource) { Models::UserSettings.new(user_id: user_id) }
describe '#can_show?' do
context 'when a user has an admin role' do
subject { described_class.new(admin, resource) }
it 'returns true' do
expect(subject.can_show?).to be_truthy
end
end
context 'when a user has a user role' do
subject { described_class.new(user, resource) }
...
end
end
end
Mit include_context :users steht alles zur Verfügung, was im Kontext definiert ist. Jedes weitere Policy-Objekt kann denselben Kontext nutzen, ohne Benutzerdeklarationen zu duplizieren.
Was gehört in einen Shared Context und was nicht?
- Gut geeignet: Variablen, die in mehr als einer Spec-Datei vorkommen. Bei Bedarf auch Mocks.
- Vorsicht: Nicht jeder Kontext lässt sich ohne ungenutzte Variablen schreiben. Das ist aber kein Problem, denn
let-Deklarationen werden erst bei Verwendung initialisiert (lazy evaluation). - Empfehlung: Kleine, fokussierte Kontexte schreiben. Nur einbinden, was gebraucht wird. Klare Benennung der Kontextdateien beibehalten.
Praktische Umsetzung: Der USEO-Ansatz
In unseren Ruby-Projekten haben wir den Einsatz von Shared Contexts über Jahre verfeinert:
- Ein Kontext pro Domäne, nicht pro Model. Statt
users.rb,orders.rb,products.rbgruppieren wir nach Geschäftslogik:authentication.rb,billing.rb. Das spiegelt die tatsächliche Verwendung wider und verhindert aufgeblähte Kontextdateien. - Override-Muster für Flexibilität. Wer im Test einen bestimmten Wert braucht, überschreibt einfach das
letin der Spec-Datei. Der Shared Context liefert sinnvolle Defaults, erzwingt aber nichts. - Shared Contexts als Dokumentation. Gut benannte Kontexte zeigen neuen Teammitgliedern sofort, welche Testdaten die Domäne erwartet. Das beschleunigt die Einarbeitung in bestehende Projekte erheblich.