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.rb gruppieren 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 let in 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.