Au travail, j'utilise minitest comme cadre de test des rails. Et dans le projet de rails, lors de la simulation et du stubing, des gemmes telles que RR et WebMock et MiniTest :: Mock, qui est une maquette standard de minitest, sont utilisées. Lors de l'écriture de tests, j'étais souvent confus sur la façon d'écrire des simulations et des stub. Cet article décrit ce qu'est un test double, quelle est la différence entre une maquette et un stub et comment utiliser chacun des RR, WebMock et MiniTest :: Mock.
Tout d'abord, qu'est-ce qu'un test double? Aussi, quelle est la différence entre mock et stub?
Il existe cinq variantes de tests doubles, selon le livre xUnit Test Pattern. J'ai souvent confondu les stubs avec des simulacres, mais ** les stubs remplacent les composants dépendants, et mock vérifie que la sortie du code testé est comme prévu **, J'ai compris ça.
référence Test Double / xUnit Patterns.com [Test Wiki double](https://ja.wikipedia.org/wiki/%E3%83%86%E3%82%B9%E3%83%88%E3%83%80%E3%83%96%E3 % 83% AB) Différences dans la simulation d'espion de talon de test automatisé
RR RR est un joyau du double framework de test de Ruby. La lecture est dite "Double Rubis". Puisque RR a un adaptateur, il semble qu'il puisse être intégré à des frameworks de test tels que RSpec, Test :: Unit et MiniTest / MiniSpec.
GitHub:https://github.com/rr/rr Officiel: http://rr.github.io/rr/
RR is a test double framework that features a rich selection of double techniques and a terse syntax. RR est un double framework de test qui présente une multitude de techniques doubles et une syntaxe concise.
RR comprend la simulation, le stub, le proxy et l'espion. C'est comme l'exemple de page RR GitHub, mais cela peut être écrit comme ceci.
Vous pouvez stub (remplacer l'appel réel) par stub
.
#Stub une méthode qui ne renvoie rien
stub(object).foo
stub(MyClass).foo
#Méthode de stub qui renvoie toujours une valeur
stub(object).foo { 'bar' }
stub(MyClass).foo { 'bar' }
#Méthode stub qui renvoie une valeur lorsqu'elle est appelée avec un argument spécifique
stub(object).foo(1, 2) { 'bar' }
stub(MyClass).foo(1, 2) { 'bar' }
Consultez la page stub pour plus de détails.
Avec mock
, vous pouvez créer une simulation qui vérifie que l'appel attendu est passé.
#Attendez-vous à ce que la méthode soit appelée
#Attendez-vous à ce que la méthode foo de l'objet soit appelée
mock(object).foo
mock(MyClass).foo
#Créez une valeur attendue dans la méthode et le stub pour toujours renvoyer la valeur spécifiée
#La méthode foo de l'objet'bar'Attendez-vous à revenir
mock(object).foo { 'bar' }
mock(MyClass).foo { 'bar' }
#Créez une valeur attendue pour une méthode avec un argument spécifique et créez un stub pour le renvoyer
#La méthode foo de l'objet a l'argument 1,Appelé par 2'bar'Attendez-vous à revenir
mock(object).foo(1, 2) { 'bar' }
mock(MyClass).foo(1, 2) { 'bar' }
Voir la page mock pour plus de détails.
Il semble que vous puissiez écrire un espion (un stub qui enregistre les informations appelées) en combinant stub
avec la description de ʻassert_received et ʻexpect (xxx) .to have_received
.
(Le GitHub officiel avait un moyen d'écrire dans Test :: Unit et Rspec, mais il ne montrait pas comment écrire dans minitest.)
# RSpec
stub(object).foo
expect(object).to have_received.foo
# Test::Unit
stub(object).foo
assert_received(object) {|o| o.foo }
Il semble que proxy
puisse être utilisé pour créer des stubs et des simulations qui interceptent et définissent de nouvelles valeurs de retour sans remplacer complètement la méthode.
#Intercepter les méthodes existantes sans les remplacer complètement
#Obtenir une nouvelle valeur de retour à partir d'une valeur existante
stub.proxy(object).foo {|str| str.upcase }
stub.proxy(MyClass).foo {|str| str.upcase }
#En plus de ce que vous faites dans l'exemple ci-dessus, créez une simulation de vos attentes
mock.proxy(object).foo {|str| str.upcase }
mock.proxy(MyClass).foo {|str| str.upcase }
#Intercepter une nouvelle méthode dans la classe et définir un double dans la valeur de retour
stub.proxy(MyClass).new {|obj| stub(obj).foo; obj }
#En plus de ce que nous faisons dans l'exemple ci-dessus.Créer une maquette de la valeur attendue dans le nouveau
mock.proxy(MyClass).new {|obj| stub(obj).foo; obj }
Pour plus de détails, voir mock.proxy, stub.proxy /blob/master/doc/03_api_overview.md#stubproxy) Voir page.
Avec ʻany_instance_of, vous pouvez stub ou simuler des méthodes lors de la création d'une instance. Vous pouvez également utiliser
stub.proxy` pour accéder à l'instance elle-même.
#Méthodes de stubbing lors de la création d'une instance de MyClass
any_instance_of(MyClass) do |klass|
stub(klass).foo { 'bar' }
end
#Une autre façon de rendre l'instance elle-même accessible
# MyClass.Stubbing de la nouvelle instance obj
stub.proxy(MyClass).new do |obj|
stub(obj).foo { 'bar' }
end
Consultez la page #any_instance_of pour plus de détails.
Si vous souhaitez utiliser l'objet uniquement pour vous moquer, vous pouvez le faire en créant un objet vide.
mock(my_mock_object = Object.new).hello
Vous pouvez également utiliser «mock!» Comme raccourci.
#vide#Créez un nouvel objet fictif avec une méthode hello et obtenez cette simulation
#Objet simulé#Peut être obtenu avec la méthode du sujet
my_mock_object = mock!.hello.subject
#dont_allow
# dont_allow
est l'opposé de # mock
et définit l'espoir que les doubles ne seront jamais appelés. Si le double est réellement appelé, vous obtiendrez une TimesCalledError
.
dont_allow(User).find('42')
User.find('42') # raises a TimesCalledError
Il semble que RR utilise # method_missing
pour définir la valeur attendue de la méthode. Cela élimine le besoin d'utiliser les méthodes # should_receive
ou # attend
.
De plus, il semble qu'il n'est pas nécessaire d'utiliser la méthode # with
pour définir la valeur attendue de l'argument. (Vous pouvez l'utiliser si vous le souhaitez)
mock(my_object).hello('bob', 'jane')
mock(my_object).hello.with('bob', 'jane') #Avec est attaché, mais comme ci-dessus
RR prend en charge l'utilisation de blocs pour définir la valeur de retour. (Vous pouvez utiliser # Returns
si vous le souhaitez)
mock(my_object).hello('bob', 'jane') { 'Hello Bob and Jane' }
mock(my_object).hello('bob', 'jane').returns('Hello Bob and Jane') #Idem que ci-dessus avec les retours
Vous pouvez ajuster le nombre attendu d'appels simulés avec les méthodes # times
, # at_least
, # at_most
et # any_times
. Vous pouvez vous attendre à ce que # with_any_args
autorise les appels avec n'importe quel argument, # with_no_args
s'attende à des appels sans arguments et # never
ne s'attende à aucune méthode à appeler.
Pour plus d'informations, consultez Présentation de l'API.
WebMock WebMock est un joyau pour définir des stubs de requête HTTP et des simulations dans Ruby. La différence avec RR est-elle la partie spécialisée dans les requêtes HTTP?
GitHub:https://github.com/bblimke/webmock
Library for stubbing and setting expectations on HTTP requests in Ruby.
Les éléments suivants sont fournis sous forme de Fonctions.
--Stab HTTP requêtes au niveau lib des clients http de bas niveau (aucune modification de test requise lors de la modification de la bibliothèque HTTP) --Paramétrage et validation des valeurs attendues pour les requêtes HTTP
Voici un extrait de l'exemple Page WebMock GitHub.
Vous pouvez stub une requête avec stub_request
.
stub_request(:any, "www.example.com") #Stub (utilisez n'importe quel)
Net::HTTP.get("www.example.com", "/") # ===> Success
#bout
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
Vous pouvez faire correspondre le corps de la demande avec le hachage lorsque le corps est URL-Encode, JSON ou XML.
#bout
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
Vous pouvez utiliser hash_including
pour faire correspondre un hachage partiel avec un corps de requête.
#corps de hachage_Correspondance avec le hachage partiel en incluant
#Peut être assemblé même si tous les corps ne correspondent pas
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
Vous pouvez faire correspondre les paramètres de requête avec des hachages.
#bout
stub_request(:get, "www.example.com").with(query: {"a" => ["b", "c"]})
RestClient.get("http://www.example.com/?a[]=b&a[]=c") # ===> Success
Comme pour le corps, hash_including
peut être utilisé pour faire correspondre les hachages partiels et les paramètres de requête.
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
Vous pouvez utiliser hash_excluding
pour faire correspondre un état qui n'est pas inclus dans le paramètre de requête.
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
Vous pouvez définir un stub qui renvoie une réponse personnalisée avec to_return
.
#bout
stub_request(:any, "www.example.com").
to_return(body: "abc", status: 200,
headers: { 'Content-Length' => 3 })
Net::HTTP.get("www.example.com", '/') # ===> "abc"
#Lever l'exception déclarée dans la classe
stub_request(:any, 'www.example.net').to_raise(StandardError)
RestClient.post('www.example.net', 'abc') # ===> StandardError
#Augmentation de l'instance d'exception
stub_request(:any, 'www.example.net').to_raise(StandardError.new("some error"))
#Lever une exception avec un message d'exception
stub_request(:any, 'www.example.net').to_raise("some error")
Vous pouvez également lever des exceptions de délai avec to_timeout
.
stub_request(:any, 'www.example.net').to_timeout
RestClient.post('www.example.net', 'abc') # ===> RestClient::RequestTimeout
Lorsque la requête est répétée, elle peut renvoyer plusieurs réponses différentes.
De plus, connectez to_return
, to_raise
et to_timeout
with then
pour renvoyer plusieurs réponses -to_raise-or-to_timeout-declarations), [utilisez times
pour spécifier le nombre de fois où renvoyer une réponse](https://github.com/bblimke/webmock#specifying-number-of-times-given-response- Vous devriez être retourné).
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"
#Une fois que toutes les réponses ont été utilisées, la dernière réponse est renvoyée indéfiniment
Net::HTTP.get('www.example.com', '/') # ===> "def\n"
Vous pouvez autoriser les requêtes sur le réseau réel avec WebMock.allow_net_connect!
. Il peut également être désactivé avec WebMock.disable_net_connect!
.
Vous pouvez également [Autoriser les demandes spécifiques mais désactiver les demandes externes](https://github.com/bblimke/webmock#external-requests-can-be-disabled- while-allowing-specific-requests) Je vais.
#Autoriser les requêtes sur le réseau réel
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.+/
#Désactiver les requêtes sur le réseau réel
WebMock.disable_net_connect!
Net::HTTP.get('www.something.com', '/') # ===> Failure
Il existe de nombreuses autres façons de stuber. Consultez la page Stubbing pour obtenir un exemple de code pour d'autres utilisations.
La page GitHub de WebMock comprend Test :: Unit et [Comment définir les valeurs attendues dans RSpec](https: // Il y avait une description de github.com/bblimke/webmock#setting-expectations-in-rspec-on-webmock-module), mais il n'y avait pas de description de minitest. Il semble que minitest puisse être écrit de la même manière que Test :: Unit (Reference of-controller-scope)).
Utilisez ʻassert_requested et ʻ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" }
Pour définir la valeur attendue à l'aide du stub, écrivez comme suit.
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 Écrivez une combinaison de «attend» et «have_requested».
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'})
Vous pouvez également écrire ce qui suit en combinant ʻa_request et
have_been_made`.
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
Pour définir la valeur attendue à l'aide du stub, écrivez comme suit.
stub = stub_request(:get, "www.example.com")
# ... make requests ...
expect(stub).to have_been_requested
Consultez la page Paramètres de valeur attendue (https://github.com/bblimke/webmock#setting-expectations) pour plus d'informations.
Réinitialisez tous les stub actuels et l'historique des demandes avec WebMock.reset!
ou WebMock.reset_executed_requests !
ne peut réinitialiser le compteur que pour les demandes effectuées.
Avec WebMock.disable!
EtWebMock.enable!
Vous pouvez désactiver ou activer WebMock, ou activer uniquement certains adaptateurs client http.
Pour d'autres fonctionnalités, consultez l'exemple de code de page WebMock GitHub (https://github.com/bblimke/webmock#examples).
MiniTest::Mock Enfin, MiniTest :: Mock est un framework d'objet simulé inclus dans minitest.
Documentation officielle: http://docs.seattlerb.org/minitest/Minitest/Mock.html
A simple and clean mock object framework. All mock objects are an instance of Mock. (Un framework d'objet simulé simple et propre. Tous les objets simulés sont des instances de MiniTest :: Mock.)
Le stub
qui poignarde un objet est une extension d'objet de Minitest :: Mock.
Les stubs ne sont valides qu'à l'intérieur du bloc et les stubs sont nettoyés à la fin du bloc. En outre, le nom de la méthode doit exister avant de pouvoir être poignardé.
La méthode stub_any_instance
vous permet de créer des stubs de méthode sur une instance d'une classe. Il peut être utilisé en installant la gemme de minitest-stub_any_instance_of.
--stub: Stub la méthode de l'objet --stub_any_instance_of: Stub la méthode d'instance de la classe
Ceci est un exemple de code de «stub».
require 'minitest/autorun'
#La classe à poignarder
class Hello
def say
'Hello!'
end
end
hello = Hello.new
#La méthode say de l'objet hello'Hello, this is from stub!'Talon à retourner
hello.stub(:say, 'Hello, this is from stub!') do
hello.say #==> "Hello, this is from stub!"
end
#Les stubs sont désactivés lorsque vous quittez le bloc
hello.say #==> "Hello!"
En utilisant stub_any_instance
, vous pouvez écrire un stub de méthode d'instance comme suit: Il semble qu'il existe de nombreuses situations où cela peut être utilisé lors de l'écriture de stubs pour des méthodes d'instance.
require 'minitest/autorun'
require 'minitest/stub_any_instance' # minitest-stub_any_instance_de gemme est également nécessaire
#La classe à poignarder
class Hello
def say
'Hello!'
end
end
#La méthode say de toute instance de la classe Hello'Hello, this is from stub!'Talon à retourner
Hello.stub_any_instance(:say, 'Hello, this is from stub!') do
Hello.new.say #==> "Hello, this is from stub!"
end
#Les stubs sont désactivés lorsque vous quittez le bloc
Hello.new.say #==> "Hello!"
`expect(name, retval, args = [], &blk) Attendez-vous à ce que le nom de la méthode soit appelé, éventuellement avec des arguments (args) ou des blocs (blk), et renvoyant une valeur de retour (retval).
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
Les arguments sont comparés aux arguments attendus en utilisant l'opérateur '===', donc des attentes moins spécifiques sont requises. (Comparez par est-il inclus?)
require 'minitest/autorun'
# users_any_Renvoie true si la méthode de chaîne est contenue dans une chaîne
@mock.expect(:uses_any_string, true, [String])
@mock.uses_any_string("foo") # => true
@mock.verify # =>true (devient vrai car le simulacre a été appelé comme prévu)
@mock.expect(:uses_one_string, true, ["foo"])
@mock.uses_one_string("bar") # =>lève MockExpectationError (car le mock n'a pas été appelé comme prévu)
Si la méthode est appelée plusieurs fois, spécifiez une nouvelle valeur attendue pour chacun. Ceux-ci sont utilisés dans l'ordre dans lequel ils sont définis.
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"
Assurez-vous que toutes les méthodes sont appelées comme prévu. Renvoie «true» s'il est appelé comme prévu. Déclenche une MockExpectationError
si l'objet fictif n'est pas appelé comme prévu.
Voir la page MiniTest :: Mock pour plus d'informations.
RR et WebMock ont tous deux suffisamment d'exemples d'utilisation dans la documentation officielle, il semble donc bon de les lire. Comme la quantité d'informations dans MiniTest :: Mock était petite, j'ai pensé qu'il serait plus facile d'imaginer si je vérifiais le mouvement de mock
et stub
avec ʻirb et
rails c`. (Au moment de l'exécution, "require'minitest / autorun" est requis.)
RR / GitHub Page RR WebMock / GitHub MiniTest::Mock MiniTest stub minitest-stub_any_instance Mock, Stub Study Group (ruby) Différences dans la simulation d'espion de talon de test automatisé Test Double / xUnit Patterns.com Utilisation de stub, simulacre avec minitest [Test Wiki 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 En regardant les variations du test double, voici [xUnit Test Patterns: Refactoring Test Code](https://www.amazon.co.jp/xUnit-Test-Patterns-Refactoring-Addison-Wesley-ebook/dp/ B004X1D36K) des livres sont parus fréquemment. Il semble que seule la version anglaise ait été publiée, mais j'ai pu confirmer le contenu sur le Web (en anglais). http://xunitpatterns.com