· 10 min read

Noch bessere Spezifikationen: Richtlinien für wartungsfreundliche Tests

Eine Reihe von Best Practices zur Unterstützung der Erstellung von Tests, die einfach zu lesen und zu warten sind (mit einem Schwerpunkt auf RSpec). Als meinungsfreudig können Sie mit einigen der Punkte nicht einverstanden sein, aber es ist gut, über dieses Zeug zu denken.

Was ist das?

Even Better Specs ist eine meinungsstarke Sammlung von Best Practices zur Unterstützung der Erstellung von Tests, die einfach zu lesen und zu warten sind.

Der Schwerpunkt liegt auf dem Ruby Testframework RSpec, aber einiges davon könnte auch auf andere Frameworks und Sprachen angewendet werden (wie sus und Crystal).

Leitprinzipien

Beschreiben Sie, was Sie testen

Seien Sie sich darüber im Klaren, was Sie testen wollen.

Schlecht

describe 'User' do
  describe 'the authenticate method for User' do
  end

  describe 'if the user is an admin' do
  end
end

Verwenden Sie die Ruby-Dokumentationskonvention von ., wenn Sie sich auf den Namen einer Klassenmethode beziehen und #, wenn Sie sich auf den Namen einer Instanzmethode beziehen.

Gut

describe User do
  describe '.authenticate' do
  end

  describe '#admin?' do
  end
end

Beschreiben Sie in Anforderungstests die Controller-Konstante und ihre Aktionen.

Gut

describe UsersController, type: :request do
  describe '#index' do
    it 'returns a successful response' do
      get users_url

      expect(response.code).to eq('200')
    end
  end

  describe '#create' do
    it 'creates a user' do
      post users_url, params: { user: { name: 'Tom Jobim' } }

      expect(User.last.name).to eq('Tom Jobim')
    end
  end
end

RuboCop

Sie können diese Richtlinie mit dem cop RSpec/DescribeClass aus dem rubocop-rspec gem erzwingen.

Diskutieren Sie diese Richtlinie

Kontexte verwenden

Kontexte sind ein wirksames Mittel, um Ihre Tests übersichtlich und gut organisiert zu gestalten. Sie sollten mit when beginnen.

Schlecht

it 'has 200 status code if logged in' do
  expect(response.code).to eq('200')
end

describe 'it returns 401 status code if not logged in' do
  it { expect(response.code).to eq('401') }
end

Gut

context 'when logged in' do
  it 'returns 200 status code' do
    expect(response.code).to eq('200')
  end
end

context 'when logged out' do
  it 'returns 401 status code' do
    expect(response.code).to eq('401')
  end
end

RuboCop

Sie können diese Richtlinie mit den Cops RSpec/ContextMethod und RSpec/ContextWording aus dem rubocop-rspec gem erzwingen.

Diskutieren Sie diese Richtlinie

Factories, not fixtures

Fabriken sind flexibler und einfacher zu handhaben. Verstehen Sie mehr hier.

Beispiel

def full_name
  "#{first_name} #{last_name}"
end```

Gut
```ruby
it 'returns the full name' do
  user = create(:user, first_name: 'Santos', last_name: 'Dumont')

  expect(user.full_name).to eq('Santos Dumont')
end

Wenn Ihr Code nicht auf die Datenbank zugreift, bevorzugen Sie build_stubbed gegenüber create, da ersteres Ihre Tests schneller macht 🔥

Gut

it 'returns the full name' do
  user = build_stubbed(:user, first_name: 'Santos', last_name: 'Dumont')

  expect(user.full_name).to eq('Santos Dumont')
end

Diskutieren Sie diesen Leitfaden

Leverage beschriebene Klasse

Tests sollen nicht DRY sein, aber das bedeutet nicht, dass wir uns umsonst wiederholen müssen. Nutzen Sie described_class, um Ihre Tests über Jahre hinweg wartbar zu machen.

Schlecht

describe Pilot do
  describe '.most_successful' do
    it 'returns the most successful pilot' do
      senna = create(:pilot, name: 'Ayrton Senna')
      create(:pilot, name: 'Alain Prost')
      create(:race, winner: senna)

      most_successful_pilot = Pilot.most_successful

      expect(most_successful_pilot.name).to eq('Ayrton Senna')
    end
  end
end

Gut

describe Pilot do
  describe '.most_successful' do
    it 'returns the most successful pilot' do
      senna = create(:pilot, name: 'Ayrton Senna')
      create(:pilot, name: 'Alain Prost')
      create(:race, winner: senna)

      most_successful_pilot = described_class.most_successful

      expect(most_successful_pilot.name).to eq('Ayrton Senna')
    end
  end
end

Wenn die Klasse Pilot jemals umbenannt wird, braucht man sie nur auf der obersten Ebene describe zu ändern.

RuboCop

Sie können diese Richtlinie mit dem cop RSpec/DescribedClass aus dem rubocop-rspec gem erzwingen.

Diskutieren Sie diese Richtlinie

Betreff verwenden

Verwenden Sie subject anstelle von described_class.new, wenn kein Argument für den Initialisierer angegeben wird.

Beispiel

class Calculator
  attr_reader :base

  def initialize(base = 0)
    @base = base
  end

  def add(number1, number2)
    base + number1 + number2
  end
end

Schlecht

describe '#add' do
 it 'sums two numbers' do
   calculator = described_class.new

   expect(calculator.add(1, 2)).to eq(3)
 end
end

Gut

describe '#add' do
  it 'sums two numbers' do
    expect(subject.add(1, 2)).to eq(3)
  end
end

Es kann überschrieben werden, wenn dem Initialisierer ein Argument gegeben wird.

describe '#add' do
  it 'sums two numbers' do
    subject = described_class.new(5)

    expect(subject.add(1, 2)).to eq(8)
  end
end

Diskutieren Sie diesen Leitfaden

Kurzbeschreibung

Eine Spezifikationsbeschreibung darf nicht länger als 100 Zeichen sein und sich nicht über mehrere Zeilen erstrecken. Wenn dies der Fall ist, sollten Sie sie mit context aufteilen.

Schlecht

it 'returns 422 when user first_name is missing from params and when user last_name is missing from params' do
end

it 'returns 422 when user first_name is missing from params \
    and when user last_name is missing from params' do
end

Gut

context 'when user first_name is missing from params' do
  it 'returns 422' do
  end
end

context 'when user last_name is missing from params' do
  it 'returns 422' do
  end
end

Diskutieren Sie diesen Leitfaden

Gruppenerwartungen

Ein it für jede Erwartung zu haben, kann zu einer schrecklichen Testleistung führen. Gruppieren Sie Erwartungen, die einen ähnlichen Datenaufbau verwenden, um die Leistung zu verbessern und die Tests besser lesbar zu machen.

Schlecht

it 'returns 200' do
  user = create(:user)

  get user_path(user)

  expect(response.code).to eq('200')
end

it 'returns JSON content type' do
  user = create(:user)

  get user_path(user)

  expect(response.content_type).to eq('application/json')
end

Gut

it 'responds with 200 http status and a JSON content type' do
  user = create(:user)

  get user_path(user)

  expect(response.code).to eq('200')
  expect(response.content_type).to eq('application/json')
end

Wir empfehlen, failure aggregation globally zu aktivieren, damit alle Fehler auf einmal aufgelistet werden.

Diskutieren Sie diese Richtlinie

Alle möglichen Fälle

Testen ist eine gute Praxis, aber wenn Sie die Randfälle nicht testen, ist es nicht sinnvoll. Testen Sie gültige, Rand- und ungültige Fälle.

Wenn Sie zu viele Fälle zu testen haben, könnte dies ein Hinweis darauf sein, dass Ihre Fachklasse zu viel tut und in andere Klassen aufgeteilt werden muss.

Beispiel

before_action :authenticate_user!
before_action :find_product

def destroy
  @product.destroy
  redirect_to products_path
end

Schlecht

describe '#destroy' do
  context 'when the product exists' do
    it 'deletes the product' do
    end
  end
end

Gut

describe '#destroy' do
  context 'when the product exists' do
    it 'deletes the product' do
    end
  end

  context 'when the product does not exist' do
    it 'raises 404' do
    end
  end

  context 'when user is not authenticated' do
    it 'raises 404' do
    end
  end
end

Diskutieren Sie diesen Leitfaden

Anfrage vs. Controller-Spezifikationen

Führen Sie Anforderungstests anstelle von Controllertests durch.

Die offizielle Empfehlung des Rails-Teams und des RSpec-Kernteams lautet, stattdessen Request-Specs zu schreiben. Anforderungsspezifikationen ermöglichen es Ihnen, sich auf eine einzelne Controller-Aktion zu konzentrieren, aber im Gegensatz zu Controllertests werden der Router, der Middleware-Stack und sowohl Rack-Anforderungen als auch -Antworten einbezogen. Dies verleiht dem Test, den Sie schreiben, mehr Realismus und hilft dabei, viele der Probleme zu vermeiden, die bei Controller-Spezifikationen häufig auftreten. In Rails 5 sind Request-Specs deutlich schneller als Request- oder Controller-Specs in Rails 4 waren.

Siehe Quelle.

Schlecht

describe UsersController, type: :controller do
  describe "#index" do
    it "returns a successful response" do
      get :index

      expect(response.code).to eq('200')
    end
  end
end

Gut

describe UsersController, type: :request do
  describe "#index" do
    it "returns a successful response" do
      user = create(:user, name: "Carlos Chagas")

      get users_path

      expect(response.code).to eq('200')
      expect(response.body).to include("Carlos Chagas")
    end
  end
end

Diskutieren Sie diesen Leitfaden

Expect vs. Should-Syntax

Verwenden Sie immer expect anstelle von should.

Schlecht

it 'creates a resource' do
  response.code.should eq('200')
end"

Gut

it 'creates a resource' do
  expect(response.code).to eq('200')
end

Diskutieren Sie diesen Leitfaden

Redundante Anforderung

Entfernen Sie alle überflüssigen require in Ihren spec-Dateien. Verwenden Sie stattdessen die Datei .rspec.

Schlecht

# spec/models/user_spec.rb
require 'rails_helper'

describe User do
end

Gut

# spec/models/user_spec.rb
describe User do
end

Gut

# .rspec
--require rails_helper

RuboCop

Sie können diese Richtlinie mit dem benutzerdefinierten Cop RSpec/RedundantRequireRailsHelper erzwingen.

Diskutieren Sie diese Richtlinie

Instanz double über double

Instance double stellt sicher, dass das Objekt auf Methodenaufrufe reagiert, was bei double nicht der Fall ist.

Beispie

class User
  def full_name
    "#{first_name} #{last_name}"
  end
end

Schlect

it "passes" do
  user = double(:user, name: "Gustavo Kuerten")
  puts user.name
end

Gut

it "fails" do
  user = instance_double(User, name: "Gustavo Kuerten")
  puts user.name
end

Der obige Test schlägt mit der folgenden Meldung fehl, da die Methode “name” nicht wirklich existiert.

Output

die Klasse User implementiert nicht die Instanzmethode: name. Vielleicht wollten Sie stattdessen `class_double` verwenden?

RuboCop

Sie können diese Richtlinie mit dem cop RSpec/VerifiedDoubles aus dem rubocop-rspec gem erzwingen.

Diskutieren Sie diese Richtlinie

Let’s not

Verwenden Sie keine let / let!. Diese neigen dazu, Ihre Tests mit der Zeit sehr kompliziert zu machen, da man die definierten Variablen nachschlagen und dann Deltas anwenden muss, um ihren aktuellen Zustand zu ermitteln. Verstehen Sie mehr hier.

Tests sollen nicht DRY sein, sondern einfach zu lesen und zu warten.

Schlecht

describe '#full_name' do
  let(:user) { build(:user, first_name: 'Edson', last_name: 'Pelé') }

  context 'when first name and last name are present' do
    it 'returns the full name' do
      expect(user.full_name).to eq('Edson Pelé')
    end
  end

  context 'when last name is not present' do
    it 'returns the first name' do
      user.last_name = nil
      expect(user.full_name).to eq('Edson')
    end
  end
end

Gut

describe '#full_name' do
  context 'when first name and last name are present' do
    it 'returns the full name' do
      user = build(:user, first_name: 'Edson', last_name: 'Pelé')

      expect(user.full_name).to eq('Edson Pelé')
    end
  end

  context 'when last name is not present' do
    it 'returns the first name' do
      user = build(:user, first_name: 'Edson', last_name: nil)

      expect(user.full_name).to eq('Edson')
    end
  end
end

RuboCop

Sie können diese Richtlinie mit dem Cop RSpec/MultipleMemoizedHelpers aus dem rubocop-rspec gem erzwingen:

Beispiel

# .rubocop.yml
RSpec/MultipleMemoizedHelpers:
  Max: 0
  AllowSubject: false

Diskutieren Sie diese Richtlinie

Vermeiden Sie Hooks

Vermeiden Sie Hooks, da sie in der Regel dazu führen, dass Ihre Tests auf lange Sicht komplexer werden.

Schlecht

describe '#index' do
  context 'when user is authenticated' do
    before do
      @user = create(:user)
      sign_in @user
      get profile_path
    end

    context 'when user has a profile' do
      it 'returns 200' do
        create(:profile, user: @user)
        expect(response.code).to eq('200')
      end
    end

    context 'when user does not have a profile' do
      it 'returns 404' do
        expect(response.code).to eq('404')
      end
    end
  end
end

Gut

describe '#index' do
  context 'when user is authenticated' do
    context 'when user has a profile' do
      it 'returns 200' do
        user = create(:user)
        create(:profile, user: user)
        sign_in user

        get profile_path

        expect(response.code).to eq('200')
      end
    end

    context 'when user does not have a profile' do
      it 'returns 404' do
        user = create(:user)
        sign_in user

        get profile_path

        expect(response.code).to eq('404')
      end
    end
  end
end

Diskutieren Sie diesen Leitfaden

Gemeinsame Beispiele ist eine Funktion, die es uns ermöglicht, Code nicht zu wiederholen, aber Tests sind kein echter Code, so dass die Einführung dieser Funktion die Komplexität Ihrer Suite erhöht.

Schlecht

shared_examples 'a normal dog' do
  it { is_expected.to be_able_to_bark }
end

describe Dog do
  subject { described_class.new(able_to_bark?: true) }
  it_behaves_like 'a normal dog'
end

gut

describe Dog do
  describe '#able_to_bark?' do
    it 'barks' do
      subject = described_class.new(able_to_bark?: true)

      expect(subject.able_to_bark?).to eq(true)
    end
  end
end

Diskutieren Sie diesen Leitfaden

Externe Abhängigkeiten vortäuschen

Testen Sie externe Abhängigkeiten zu Ihrem Testobjekt. Mit externen Abhängigkeiten meine ich Code, der nicht in die Hauptverantwortung des Testobjekts fällt.

Beispiel

def github_stars(repository_id)
  stars = Github.fetch_repository_stars(repository_id)
  "Stars: #{stars}"
end

Schlecht

describe '#github_stars' do
  it 'displays the number of stars' do
    expect(subject.github_stars(1)).to eq('Stars: 10')
  end
end

Gut

describe '#github_stars' do
  it 'displays the number of stars' do
    expect(Github).to receive(:fetch_repository_stars).with(1).and_return(10)

    expect(subject.github_stars(1)).to eq('Stars: 10')
  end
end

Es sei denn, Sie wollen absichtlich die zugrunde liegenden Abhängigkeiten testen. Diese Art von Test muss jedoch auf sehr wenige Fälle beschränkt sein, sonst wird Ihre Testsuite langsam und schwer zu pflegen.

Beispiel

def process(payload)
  UserCreator.new(payload).create
  ProductCreator.new(payload).create
end

Gut

describe '#process' do
  it 'creates a user and a product' do
    payload = { user: { name: 'Isabel' }, product: { name: 'Book' } }

    subject.process(payload)

    expect(User.count).to eq(1)
    expect(Product.count).to eq(1)
  end
end

Diskutieren Sie diesen Leitfaden

HTTP-Anfragen stoppen

Manchmal müssen Sie auf externe Dienste zugreifen. In diesen Fällen können Sie sich nicht auf den echten Dienst verlassen, sondern Sie sollten ihn mit Lösungen wie webmock oder VCR stubben.

Beispiel

def request
  uri = URI.parse("http://www.example.com/")
  request = Net::HTTP::Post.new(uri.path)
  request['Content-Length'] = 3

  Net::HTTP.start(uri.host, uri.port) do |http|
    http.request(request, "abc")
  end
end

Schlecht

describe '#request' do
  response = subject.request

  expect(response.code).to eq('200')
end

Gut

describe '#request' do
  stub_request(:post, "www.example.com").with(body: "abc", headers: { 'Content-Length' => 3 })

  response = subject.request

  expect(response.code).to eq('200')
end

Diskutieren Sie diesen Leitfaden

Stub Umgebungsvariablen

Ziehen Sie es vor, Umgebungsvariablen zu stubben, wann immer es möglich ist, damit Ihre Tests in sich geschlossener werden. Der stub_env gem kann Ihnen dabei helfen.

Beispiel

def notify_sales_team
  mail(
    to: ENV['SALES_TEAM_EMAIL'],
    subject: 'New sale'
  )
end

Schlecht

describe '#notify_sales_team' do
  it 'prepares the email' do
    subject = described_class.notify_sales_team

    expect(subject.to).to eq(['sales-group@company.com'])
    expect(subject.subject).to eq('New sale')
  end
end

Schlecht

# .env.test
SALES_TEAM_EMAIL='sales-group@company.com'

Gut

describe '#notify_sales_team' do
  it 'prepares the email' do
    stub_env('SALES_TEAM_EMAIL', 'sales-group@company.com')

    subject = described_class.notify_sales_team

    expect(subject.to).to eq(['sales-group@company.com'])
    expect(subject.subject).to eq('New sale')
  end
end

Diesen Leitfaden diskutieren

Erstellen Sie nur die Daten, die Sie benötigen

Erstellen Sie nur die Daten, die Sie für jeden Test benötigen. Wenn Sie mehr Daten als nötig erstellen, kann dies Ihre Testsuite verlangsamen.

Schlecht

describe '.featured_product' do
  it 'returns the featured product' do
    create_list(:product, 5)
    product_featured = create(:product, featured: true)

    expect(described_class.featured_product).to eq(product_featured)
  end
end

Gut

describe '.featured_product' do
  it 'returns the featured product' do
    create(:product, featured: false)
    product_featured = create(:product, featured: true)

    expect(described_class.featured_product).to eq(product_featured)
  end
end

Diskutieren Sie diesen Leitfaden

Wer verwendet sie

Hier sind einige Beispiele von Organisationen, die Even Better Specs verwenden:

Nutzt Ihr Team es auch? Lassen Sie es uns wissen!


Back to Blog

Related Posts

View All Posts »

Leichte Einführung in mruby

Ruby 3.3.5 wurde veröffentlicht. Das Update behebt kleinere Fehler und wird allen Nutzern empfohlen. Weitere Details sind in den GitHub Release Notes verfügbar.

Ruby on Rails World 2024

Rails 8 verfolgt einen radikal vereinfachten Ansatz, um die Komplexität moderner Webentwicklung zu reduzieren. Mit dem \#NOBUILD-Prinzip werden CSS und JavaScript ohne Build-Prozesse direkt an den Browser geliefert. Die neuen Technologien Propshaft, Solid Cable und Solid Queue ermöglichen performante Lösungen ohne den Einsatz externer Dienste. Rails will damit die Abhängigkeit von teuren PaaS-Diensten minimieren und setzt auf offene, kostengünstige Alternativen für die Bereitstellung auf eigener Hardware. Gleichzeitig werden leistungsfähige Tools wie Thruster und Kamal 2 eingeführt, die Deployment-Prozesse weiter optimieren. Rails 8 kombiniert bewährte Prinzipien mit innovativen Features, um Entwicklern maximale Flexibilität und Effizienz zu bieten.

Ruby 3.3.5 Released

Ruby 3.3.5 wurde veröffentlicht. Das Update behebt kleinere Fehler und wird allen Nutzern empfohlen. Weitere Details sind in den GitHub Release Notes verfügbar.