Bei der Arbeit verwende ich Minitest als Schienen-Test-Framework. Und im Rails-Projekt werden beim Verspotten und Stubing Edelsteine wie RR und WebMock sowie MiniTest :: Mock verwendet, ein Standard-Minitest-Mock. Beim Schreiben von Tests war ich oft verwirrt darüber, wie man Mock and Stub schreibt. Dieser Artikel beschreibt, was ein Testdouble ist, was der Unterschied zwischen einem Mock und einem Stub ist und wie RR, WebMock und MiniTest :: Mock verwendet werden.
Was ist ein Testdoppel? Was ist der Unterschied zwischen Mock und Stub?
Laut dem Buch xUnit Test Patterns (http://xunitpatterns.com) gibt es fünf Varianten von Test-Doubles. Ich habe Stubs oft mit Spott verwechselt, aber ** Stubs ersetzen abhängige Komponenten, und Spott bestätigt, dass die Ausgabe des zu testenden Codes wie erwartet ist **, Ich habe das verstanden.
Referenz Test Double / xUnit Patterns.com [Wiki test double](https://ja.wikipedia.org/wiki/%E3%83%86%E3%82%B9%E3%83%88%E3%83%80%E3%83%96%E3 % 83% AB) Unterschiede im automatisierten Teststub-Spionagemodell
RR RR ist ein Juwel von Rubys Test-Double-Framework. Die Lesung soll "Double Ruby" sein. Da RR über einen Adapter verfügt, kann es anscheinend in Test-Frameworks wie RSpec, Test :: Unit und MiniTest / MiniSpec integriert werden.
GitHub:https://github.com/rr/rr Beamter: http://rr.github.io/rr/
RR is a test double framework that features a rich selection of double techniques and a terse syntax. RR ist ein Test-Double-Framework, das eine Fülle von Double-Techniken und eine präzise Syntax bietet.
RR enthält Mock, Stub, Proxy und Spy. Es ist ein Beispiel für RR GitHub-Seite, aber es kann so geschrieben werden.
Sie können den eigentlichen Anruf durch "stub" stubben (ersetzen).
#Stub eine Methode, die nichts zurückgibt
stub(object).foo
stub(MyClass).foo
#Stub-Methode, die immer einen Wert zurückgibt
stub(object).foo { 'bar' }
stub(MyClass).foo { 'bar' }
#Stub-Methode, die einen Wert zurückgibt, wenn sie mit einem bestimmten Argument aufgerufen wird
stub(object).foo(1, 2) { 'bar' }
stub(MyClass).foo(1, 2) { 'bar' }
Weitere Informationen finden Sie auf der Seite stub.
Mit mock
können Sie ein Mock erstellen, das überprüft, ob der erwartete Anruf getätigt wird.
#Erwarten Sie, dass die Methode aufgerufen wird
#Erwarten Sie, dass die foo-Methode des Objekts aufgerufen wird
mock(object).foo
mock(MyClass).foo
#Erstellen Sie einen erwarteten Wert in der Methode und im Stub, um immer den angegebenen Wert zurückzugeben
#Die foo-Methode des Objekts'bar'Erwarten Sie, zurückzukehren
mock(object).foo { 'bar' }
mock(MyClass).foo { 'bar' }
#Erstellen Sie einen erwarteten Wert für eine Methode mit einem bestimmten Argument und erstellen Sie einen Stub, um ihn zurückzugeben
#Die foo-Objektmethode hat Argument 1,Aufgerufen von 2'bar'Erwarten Sie, zurückzukehren
mock(object).foo(1, 2) { 'bar' }
mock(MyClass).foo(1, 2) { 'bar' }
Weitere Informationen finden Sie auf der Seite mock.
Es scheint, dass Sie einen Spion schreiben können (einen Stub, der die aufgerufenen Informationen aufzeichnet), indem Sie "stub" mit der Beschreibung von "assert_received" und "expected (xxx) .to have_received" kombinieren. (Der offizielle GitHub hatte eine Möglichkeit, in Test :: Unit und Rspec zu schreiben, aber er zeigte nicht, wie man in Minitest schreibt.)
# RSpec
stub(object).foo
expect(object).to have_received.foo
# Test::Unit
stub(object).foo
assert_received(object) {|o| o.foo }
Es scheint, dass "Proxy" verwendet werden kann, um Stubs zu erstellen und diese abzufangen und neue Rückgabewerte festzulegen, ohne die Methode vollständig zu überschreiben.
#Fangen Sie vorhandene Methoden ab, ohne sie vollständig zu überschreiben
#Holen Sie sich einen neuen Rückgabewert von einem vorhandenen Wert
stub.proxy(object).foo {|str| str.upcase }
stub.proxy(MyClass).foo {|str| str.upcase }
#Erstellen Sie zusätzlich zu dem, was Sie im obigen Beispiel tun, ein Modell Ihrer Erwartungen
mock.proxy(object).foo {|str| str.upcase }
mock.proxy(MyClass).foo {|str| str.upcase }
#Fangen Sie eine neue Methode in der Klasse ab und definieren Sie ein Double im Rückgabewert
stub.proxy(MyClass).new {|obj| stub(obj).foo; obj }
#Zusätzlich zu dem, was wir im obigen Beispiel tun.Erstellen Sie ein Modell des erwarteten Werts in new
mock.proxy(MyClass).new {|obj| stub(obj).foo; obj }
Weitere Informationen finden Sie unter mock.proxy, stub.proxy. /blob/master/doc/03_api_overview.md#stubproxy) Siehe Seite.
Mit any_instance_of
können Sie beim Erstellen einer Instanz Methoden stubben oder verspotten. Sie können auch stub.proxy
verwenden, um auf die Instanz selbst zuzugreifen.
#Stubbing-Methoden beim Erstellen einer Instanz von MyClass
any_instance_of(MyClass) do |klass|
stub(klass).foo { 'bar' }
end
#Eine andere Möglichkeit, die Instanz selbst zugänglich zu machen
# MyClass.Stubbing der neuen Instanz obj
stub.proxy(MyClass).new do |obj|
stub(obj).foo { 'bar' }
end
Weitere Informationen finden Sie auf der Seite #any_instance_of.
Wenn Sie das Objekt nur zum Verspotten verwenden möchten, können Sie dies tun, indem Sie ein leeres Objekt erstellen.
mock(my_mock_object = Object.new).hello
Sie können auch mock!
Als Verknüpfung verwenden.
#leer#Erstellen Sie ein neues Mock-Objekt mit einer Hallo-Methode und holen Sie sich dieses Mock
#Scheinobjekt#Kann mit der Subjektmethode erhalten werden
my_mock_object = mock!.hello.subject
#dont_allow
# dont_allow
ist das Gegenteil von # mock
und setzt die Erwartung, dass Doubles niemals aufgerufen werden. Wenn das Double tatsächlich aufgerufen wird, erhalten Sie einen "TimesCalledError".
dont_allow(User).find('42')
User.find('42') # raises a TimesCalledError
Es scheint, dass RR # method_missing
verwendet, um den erwarteten Wert der Methode festzulegen. Dadurch entfällt die Notwendigkeit, die Methoden "# should_receive" oder "# expected" zu verwenden.
Es scheint auch nicht erforderlich zu sein, die Methode "# with" zu verwenden, um den erwarteten Wert des Arguments festzulegen. (Sie können es verwenden, wenn Sie möchten)
mock(my_object).hello('bob', 'jane')
mock(my_object).hello.with('bob', 'jane') #Mit ist beigefügt, aber das gleiche wie oben
RR unterstützt die Verwendung von Blöcken zum Festlegen des Rückgabewerts. (Sie können "# return" verwenden, wenn Sie möchten)
mock(my_object).hello('bob', 'jane') { 'Hello Bob and Jane' }
mock(my_object).hello('bob', 'jane').returns('Hello Bob and Jane') #Gleich wie oben bei Rücksendungen
Sie können die erwartete Anzahl von Scheinaufrufen mit den Methoden "# times", "# at_least", "# at_most" und "# any_times" anpassen. Sie können erwarten, dass # with_any_args
Aufrufe mit beliebigen Argumenten zulässt, # with_no_args
Aufrufe ohne Argumente erwartet und # never
erwartet, dass keine Methoden aufgerufen werden.
Weitere Informationen finden Sie unter API-Übersicht.
WebMock WebMock ist ein Juwel zum Festlegen von HTTP-Anforderungsstubs und zum Verspotten in Ruby. Ist der Unterschied zu RR der Teil, der sich auf HTTP-Anforderungen spezialisiert hat?
GitHub:https://github.com/bblimke/webmock
Library for stubbing and setting expectations on HTTP requests in Ruby.
Folgendes wird als [Funktionen] bereitgestellt (https://github.com/bblimke/webmock#features).
--Stab-HTTP-Anforderungen auf lib-Ebene von http-Clients auf niedriger Ebene (keine Teständerungen beim Ändern der HTTP-Bibliothek erforderlich)
Hier ist ein Auszug aus dem Beispiel WebMock GitHub-Seite.
Sie können eine Anfrage mit stub_request
stubben.
stub_request(:any, "www.example.com") #Stub (benutze einen)
Net::HTTP.get("www.example.com", "/") # ===> Success
#Stummel
stub_request(:post, "www.example.com").
with(body: "abc", headers: { 'Content-Length' => 3 })
uri = URI.parse("http://www.example.com/")
req = Net::HTTP::Post.new(uri.path)
req['Content-Length'] = 3
res = Net::HTTP.start(uri.host, uri.port) do |http|
http.request(req, "abc")
end # ===> Success
Sie können den Anforderungshauptteil mit dem Hash abgleichen, wenn der Hauptteil URL-Encode, JSON oder XML ist.
#Stummel
stub_request(:post, "www.example.com").
with(body: {data: {a: '1', b: 'five'}})
RestClient.post('www.example.com', "data[a]=1&data[b]=five",
content_type: 'application/x-www-form-urlencoded') # ===> Success
RestClient.post('www.example.com', '{"data":{"a":"1","b":"five"}}',
content_type: 'application/json') # ===> Success
RestClient.post('www.example.com', '<data a="1" b="five" />',
content_type: 'application/xml') # ===> Success
Sie können "hash_including" verwenden, um einen Teil-Hash mit einem Anforderungshauptteil abzugleichen.
#Hash Körper_Übereinstimmung mit partiellem Hash beim Einschließen
#Kann auch dann zusammengestellt werden, wenn nicht alle Körper übereinstimmen
stub_request(:post, "www.example.com").
with(body: hash_including({data: {a: '1', b: 'five'}}))
RestClient.post('www.example.com', "data[a]=1&data[b]=five&x=1",
:content_type => 'application/x-www-form-urlencoded') # ===> Success
Sie können Abfrageparameter mit Hashes abgleichen.
#Stummel
stub_request(:get, "www.example.com").with(query: {"a" => ["b", "c"]})
RestClient.get("http://www.example.com/?a[]=b&a[]=c") # ===> Success
Wie beim Body kann "hash_including" verwendet werden, um partielle Hashes und Abfrageparameter abzugleichen.
stub_request(:get, "www.example.com").
with(query: hash_including({"a" => ["b", "c"]}))
RestClient.get("http://www.example.com/?a[]=b&a[]=c&x=1") # ===> Success
Sie können "hash_excluding" verwenden, um einem Status zu entsprechen, der nicht im Abfrageparameter enthalten ist.
stub_request(:get, "www.example.com").
with(query: hash_excluding({"a" => "b"}))
RestClient.get("http://www.example.com/?a=b") # ===> Failure
RestClient.get("http://www.example.com/?a=c") # ===> Success
Sie können einen Stub festlegen, der eine benutzerdefinierte Antwort mit "to_return" zurückgibt.
#Stummel
stub_request(:any, "www.example.com").
to_return(body: "abc", status: 200,
headers: { 'Content-Length' => 3 })
Net::HTTP.get("www.example.com", '/') # ===> "abc"
#Lösen Sie die in der Klasse deklarierte Ausnahme aus
stub_request(:any, 'www.example.net').to_raise(StandardError)
RestClient.post('www.example.net', 'abc') # ===> StandardError
#Auslösen der Ausnahmeinstanz
stub_request(:any, 'www.example.net').to_raise(StandardError.new("some error"))
#Auslösen einer Ausnahme mit einer Ausnahmemeldung
stub_request(:any, 'www.example.net').to_raise("some error")
Sie können auch Timeout-Ausnahmen mit to_timeout
auslösen.
stub_request(:any, 'www.example.net').to_timeout
RestClient.post('www.example.net', 'abc') # ===> RestClient::RequestTimeout
Wenn die Anforderung wiederholt wird, können mehrere unterschiedliche Antworten zurückgegeben werden.
Außerdem verbinde to_return
, to_raise
und to_timeout
mit then
, um mehrere Antworten zurückzugeben -to_raise-or-to_timeout-Deklarationen), [verwenden Sie times
, um anzugeben, wie oft eine Antwort zurückgegeben werden soll](https://github.com/bblimke/webmock#specifying-number-of-times-given-response- Sie sollten zurückgegeben werden).
stub_request(:get, "www.example.com").
to_return({body: "abc"}, {body: "def"})
Net::HTTP.get('www.example.com', '/') # ===> "abc\n"
Net::HTTP.get('www.example.com', '/') # ===> "def\n"
#Nachdem alle Antworten verwendet wurden, wird die letzte Antwort auf unbestimmte Zeit zurückgegeben
Net::HTTP.get('www.example.com', '/') # ===> "def\n"
Sie können Anforderungen an das tatsächliche Netzwerk mit "WebMock.allow_net_connect!" Zulassen. Es kann auch mit "WebMock.disable_net_connect!" Deaktiviert werden. Sie können auch bestimmte Anforderungen zulassen, aber externe Anforderungen deaktivieren Ich werde.
#Zulassen von Anforderungen an das tatsächliche Netzwerk
WebMock.allow_net_connect!
stub_request(:any, "www.example.com").to_return(body: "abc")
Net::HTTP.get('www.example.com', '/') # ===> "abc"
Net::HTTP.get('www.something.com', '/') # ===> /.+Something.+/
#Deaktivieren Sie Anforderungen an das tatsächliche Netzwerk
WebMock.disable_net_connect!
Net::HTTP.get('www.something.com', '/') # ===> Failure
Es gibt viele andere Möglichkeiten, wie Sie stubben können. Auf der Seite Stubbing finden Sie Beispielcode für andere Verwendungszwecke.
Die GitHub-Seite von WebMock enthält Test :: Unit und [Festlegen der erwarteten Werte in RSpec](https: // Es gab eine Beschreibung von github.com/bblimke/webmock#setting-expectations-in-rspec-on-webmock-module), aber es gab keine Beschreibung von minitest. Es scheint, dass Minitest auf die gleiche Weise geschrieben werden kann wie Test :: Unit (Referenz of-controller-scope)).
Verwenden Sie "assert_requested" oder "assert_not_requested".
require 'webmock/test_unit'
stub_request(:any, "www.example.com")
uri = URI.parse('http://www.example.com/')
req = Net::HTTP::Post.new(uri.path)
req['Content-Length'] = 3
res = Net::HTTP.start(uri.host, uri.port) do |http|
http.request(req, 'abc')
end
assert_requested :post, "http://www.example.com",
headers: {'Content-Length' => 3}, body: "abc",
times: 1 # ===> Success
assert_not_requested :get, "http://www.something.com" # ===> Success
assert_requested(:post, "http://www.example.com",
times: 1) { |req| req.body == "abc" }
Schreiben Sie wie folgt, um den erwarteten Wert mithilfe des Stubs festzulegen.
stub_get = stub_request(:get, "www.example.com")
stub_post = stub_request(:post, "www.example.com")
Net::HTTP.get('www.example.com', '/')
assert_requested(stub_get)
assert_not_requested(stub_post)
Rspec Schreiben Sie eine Kombination aus "erwarten" und "haben_erfordert".
require 'webmock/rspec'
expect(WebMock).to have_requested(:get, "www.example.com").
with(body: "abc", headers: {'Content-Length' => 3}).twice
expect(WebMock).not_to have_requested(:get, "www.something.com")
expect(WebMock).to have_requested(:post, "www.example.com").
with { |req| req.body == "abc" }
# Note that the block with `do ... end` instead of curly brackets won't work!
# Why? See this comment https://github.com/bblimke/webmock/issues/174#issuecomment-34908908
expect(WebMock).to have_requested(:get, "www.example.com").
with(query: {"a" => ["b", "c"]})
expect(WebMock).to have_requested(:get, "www.example.com").
with(query: hash_including({"a" => ["b", "c"]}))
expect(WebMock).to have_requested(:get, "www.example.com").
with(body: {"a" => ["b", "c"]},
headers: {'Content-Type' => 'application/json'})
Sie können auch Folgendes schreiben, indem Sie "a_request" und "have_been_made" kombinieren.
expect(a_request(:post, "www.example.com").
with(body: "abc", headers: {'Content-Length' => 3})).
to have_been_made.once
expect(a_request(:post, "www.something.com")).to have_been_made.times(3)
expect(a_request(:post, "www.something.com")).to have_been_made.at_least_once
expect(a_request(:post, "www.something.com")).
to have_been_made.at_least_times(3)
expect(a_request(:post, "www.something.com")).to have_been_made.at_most_twice
expect(a_request(:post, "www.something.com")).to have_been_made.at_most_times(3)
expect(a_request(:any, "www.example.com")).not_to have_been_made
expect(a_request(:post, "www.example.com").with { |req| req.body == "abc" }).
to have_been_made
expect(a_request(:get, "www.example.com").with(query: {"a" => ["b", "c"]})).
to have_been_made
expect(a_request(:get, "www.example.com").
with(query: hash_including({"a" => ["b", "c"]}))).to have_been_made
expect(a_request(:post, "www.example.com").
with(body: {"a" => ["b", "c"]},
headers: {'Content-Type' => 'application/json'})).to have_been_made
Schreiben Sie wie folgt, um den erwarteten Wert mithilfe des Stubs festzulegen.
stub = stub_request(:get, "www.example.com")
# ... make requests ...
expect(stub).to have_been_requested
Weitere Informationen finden Sie auf der Seite Einstellungen für erwartete Werte (https://github.com/bblimke/webmock#setting-expectations).
Setzen Sie alle aktuellen Stubs und den Anforderungsverlauf mit WebMock.reset!
oder WebMock.reset_executed_requests] zurück !
kann den Zähler nur für gestellte Anforderungen zurücksetzen.
Mit WebMock.disable!
UndWebMock.enable!
Sie können WebMock deaktivieren oder aktivieren oder nur einige http-Client-Adapter aktivieren.
Weitere Funktionen finden Sie unter Beispielcode für die WebMock GitHub-Seite (https://github.com/bblimke/webmock#examples).
MiniTest::Mock Schließlich ist MiniTest :: Mock ein in minitest enthaltenes Mock-Object-Framework.
Offizielle Dokumentation: http://docs.seattlerb.org/minitest/Minitest/Mock.html
A simple and clean mock object framework. All mock objects are an instance of Mock. (Ein einfaches und sauberes Mock-Objekt-Framework. Alle Mock-Objekte sind Instanzen von MiniTest :: Mock.)
Ein Objekt erstechen stub
ist eine Objekterweiterung von Minitest :: Mock.
Stubs sind nur innerhalb des Blocks gültig und die Stubs werden am Ende des Blocks bereinigt. Außerdem muss der Methodenname vorhanden sein, bevor er erstochen werden kann.
Mit der Methode stub_any_instance
können Sie einen Methodenstub für eine Instanz einer Klasse erstellen. Es kann verwendet werden, indem das Juwel von minitest-stub_any_instance_of installiert wird.
--stub: Stub die Methode des Objekts --stub_any_instance_of: Stub die Instanzmethode der Klasse
Dies ist ein Beispielcode von stub
.
require 'minitest/autorun'
#Die Klasse, die erstochen werden soll
class Hello
def say
'Hello!'
end
end
hello = Hello.new
#Die Say-Methode des Hallo-Objekts'Hello, this is from stub!'Stub, um zurückzukehren
hello.stub(:say, 'Hello, this is from stub!') do
hello.say #==> "Hello, this is from stub!"
end
#Stubs werden deaktiviert, wenn Sie den Block verlassen
hello.say #==> "Hello!"
Mit stub_any_instance
können Sie einen Instanzmethodenstub wie folgt schreiben: Es scheint, dass es viele Situationen gibt, in denen dies beim Schreiben von Stubs zum Beispiel Methoden verwendet werden kann.
require 'minitest/autorun'
require 'minitest/stub_any_instance' # minitest-stub_any_instance_von Edelstein wird auch benötigt
#Die Klasse, die erstochen werden soll
class Hello
def say
'Hello!'
end
end
#Die say-Methode einer beliebigen Instanz der Hello-Klasse'Hello, this is from stub!'Stub, um zurückzukehren
Hello.stub_any_instance(:say, 'Hello, this is from stub!') do
Hello.new.say #==> "Hello, this is from stub!"
end
#Stubs werden deaktiviert, wenn Sie den Block verlassen
Hello.new.say #==> "Hello!"
`expect(name, retval, args = [], &blk) Erwarten Sie, dass der Methodenname aufgerufen wird, optional mit Argumenten (args) oder Blöcken (blk), und einen Rückgabewert (retval) zurückgibt.
require 'minitest/autorun'
@mock.expect(:meaning_of_life, 42)
@mock.meaning_of_life # => 42
@mock.expect(:do_something_with, true, [some_obj, true])
@mock.do_something_with(some_obj, true) # => true
@mock.expect(:do_something_else, true) do |a1, a2|
a1 == "buggs" && a2 == :bunny
end
Argumente werden mit dem Operator '===' mit den erwarteten Argumenten verglichen, sodass weniger spezifische Erwartungswerte erforderlich sind. (Vergleichen mit ist es enthalten?)
require 'minitest/autorun'
# users_any_Gibt true zurück, wenn die String-Methode in einem String enthalten ist
@mock.expect(:uses_any_string, true, [String])
@mock.uses_any_string("foo") # => true
@mock.verify # =>true (wird true, weil der Mock wie erwartet aufgerufen wurde)
@mock.expect(:uses_one_string, true, ["foo"])
@mock.uses_one_string("bar") # =>löst MockExpectationError aus (weil der Mock nicht wie erwartet aufgerufen wurde)
Wenn die Methode mehrmals aufgerufen wird, geben Sie jeweils einen neuen erwarteten Wert an. Diese werden in der Reihenfolge verwendet, in der sie definiert sind.
require 'minitest/autorun'
@mock.expect(:ordinal_increment, 'first')
@mock.expect(:ordinal_increment, 'second')
@mock.ordinal_increment # => 'first'
@mock.ordinal_increment # => 'second'
@mock.ordinal_increment # => raises MockExpectationError "No more expects available for :ordinal_increment"
Stellen Sie sicher, dass alle Methoden wie erwartet aufgerufen werden. Gibt "true" zurück, wenn es wie erwartet aufgerufen wird. Löst einen MockExpectationError
aus, wenn das Mock-Objekt nicht wie erwartet aufgerufen wird.
Weitere Informationen finden Sie auf der Seite MiniTest :: Mock.
Sowohl RR als auch WebMock haben genügend Verwendungsbeispiele in der offiziellen Dokumentation, daher scheint es gut, sie zu lesen. Da die Informationsmenge in MiniTest :: Mock gering war, dachte ich, es wäre leichter vorstellbar, wenn ich die Bewegung von "mock" und "stub" mit "irb" und "Rails c" überprüfen würde. (Zur Laufzeit ist "require'minitest / autorun" erforderlich.)
RR / GitHub RR-Seite WebMock / GitHub MiniTest::Mock MiniTest stub minitest-stub_any_instance Mock, Stub Study Group (Ruby) Unterschiede im automatisierten Teststub-Spionagemodell Test Double / xUnit Patterns.com Mit Stub mit Minitest verspotten [Wiki test double](https://ja.wikipedia.org/wiki/%E3%83%86%E3%82%B9%E3%83%88%E3%83%80%E3%83%96%E3 % 83% AB)
xUnit Test Pattern In Bezug auf die Variationen des Testdoppels finden Sie hier xUnit-Testmuster: Refactoring-Testcode. B004X1D36K) Bücher werden häufig veröffentlicht. Es scheint, dass nur die englische Version veröffentlicht wurde, aber ich konnte den Inhalt im Web (auf Englisch) bestätigen. http://xunitpatterns.com