Dank rspec-expectations, ist es ziemlich leicht, in RSpec einfache eigene Matcher zu definieren. Die Matcher können sogar in einer sehr kurzen Macroartigen Notation verwendet werden.
Wenn Matcher Logik allerdings etwas komplexer wird, oder aber auch für andere Ruby Testumgebungen (z.B. MiniTest) gültig sein soll, muss eine eigene Matcherklasse gebaut werden.
Eine Klasse und seine Spec
Zunächst die Ausgangssituation: eine Product Spec, die einen Alias testet:
# spec/product_spec.rb
require 'rails_helper'
RSpec.describe Product do
subject { Product.new 'Car' }
describe '#to_s' do
it 'returns the name' do
expect(subject.to_s).to eq(subject.name)
end
end
end
Das Produkt hat einen Namen (name) und den Alias to_s:
class Product
attr_reader :name
alias_method :name, :to_s
def initialize(name)
@name = name
end
end
PORO Matcher Klasse
Die Matcher Klasse ist ein PORO, das mindestens 4 Methoden implementiert:
- #description: Beschreibung des Matchers
- #failure_message: Nachricht, wenn der Test fehl schlägt (expect(subject).to match(something))
- #negative_failure_message: Nachricht, wenn der negierte Test fehl schlägt (expect(subject).to_not match(something))
- #matches?: der eigentliche Vergleich
# support/matchers/alias_method_matcher.rb
class AliasMethodMatcher
def initialize(original_method)
@original_method = original_method
end
def with(alias_method)
@alias_method = alias_method
self
end
def matches?(subject)
raise 'No aliased method provided' if @alias_method.nil?
subject.method(@original_method) == subject.method(@alias_method)
end
def failure_message
"Expected ##{@original_method} to be aliased by ##{@alias_method}, but it is not"
end
def failure_message_when_negated
"Expected ##{@original_method} to not be aliased by ##{@alias_method}, but it is"
end
def description
"#{@alias_method} should be an alias of method #{@original_method}"
end
end
Der Matcher profitiert von einer einfachen Technik. Setter Methoden (with) werden verkettbar, indem sie das Matcher Objekt zurückgeben.
Erst am Ende der Methodenkette wird automatisch matches? aufgerufen.
Eine Helper Funktion muss das Matcher Objekt instantiieren:
# support/matchers/alias_method_matcher.rb
def alias_method(method)
AliasMethodMatcher.new(method)
end
Die aktuelle Implementierung von Shoulda Matchers verwendet den selben Ansatz.
Die gleiche Spec, aber mit dem Matcher:
# spec/product_spec.rb
require 'rails_helper'
RSpec.describe Product do
subject { Product.new 'Car' }
it { is_expected.to alias_method(:name).with(:to_s) }
end
Der Helper alias_method gibt eine Instanz des AliasMethodMatcher zurück. An das Matcher Objekt wird with mit dem Argument to_s gesendet. Diese Methode gibt ebenfalls das Matcher Objekt (sich selbst) zurück. Em Ende des it Blocks wird automatisch matches? an das Matches Objekt gesendet und der Vergleich ausgeführt.
Der Spec Helper
Wenn der Custom Matcher nicht explizit in jeder Spec eingebunden werden soll, kann der Matcher auch global im Spec Helper geladen werden:
# spec/spec_helper.rb
require File.join(File.dirname(__FILE__), 'support', 'alias_method_matcher.rb')
Vielen Dank an Arthus Shagall, für hilfreiche Hinweise, wie das Beispiel verbessert werden kann.