· 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
- Tests müssen in sich geschlossen sein, nicht DRY
- Tests sollten dem Arrange-Act-Assert Muster folgen
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
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!