[RUBY] Mock et stub avec minitest (utilisez RR, WebMock, MiniTest :: Mock)

À propos de cet article

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.

table des matières

Qu'est-ce qu'un test double (mock, stub)?

Tout d'abord, qu'est-ce qu'un test double? Aussi, quelle est la différence entre mock et stub?

5 variations de test double

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.

  1. Talon de test
  1. Test d'espion
  1. Objet factice
  1. Faux objet --Un objet qui remplace les composants dépendants du code sous test
  1. Objet factice --Utilisez un objet factice si vous avez besoin d'un objet comme paramètre dans la signature de méthode du code sous test (si ni le test ni le code sous test ne se soucient de cet objet)

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.

Comment utiliser RR

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.

bout

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.

moquer

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.

espion

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 }

Procuration

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.

Instance de classe

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.

Objets purement simulés

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

Autre

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

Comment utiliser WebMock

Voici un extrait de l'exemple Page WebMock GitHub.

bout

Vous pouvez stub une requête avec stub_request.

Requête avec stub basée uniquement sur l'URI et la réponse par défaut

stub_request(:any, "www.example.com")    #Stub (utilisez n'importe quel)
Net::HTTP.get("www.example.com", "/")    # ===> Success

Demandes de stub basées sur la méthode, l'URI, le corps, l'en-tête (https://github.com/bblimke/webmock#stubbing-requests-based-on-method-uri-body-and-headers)

#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

Faire correspondre le corps de la demande avec le hachage (https://github.com/bblimke/webmock#matching-request-body-against-a-hash-body-can-be-url-encoded-json-or-xml)

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

Faire correspondre les paramètres de requête (https://github.com/bblimke/webmock#matching-query-params-using-hash)

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

Stub qui renvoie une réponse personnalisée

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"

Soulever des erreurs

#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

Plusieurs réponses différentes à des demandes répétées

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"

Autoriser ou désactiver les requêtes réelles sur le réseau (https://github.com/bblimke/webmock#real-requests-to-network-can-be-allowed-or-disabled)

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.

Réglage de la valeur attendue (simulé)

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)).

Test::Unit/minitest

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.

Autre

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.)

Comment utiliser MiniTest :: Mock

bout

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!"

moquer

attendre la méthode

`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"

vérifier la méthode

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.

finalement

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.)

Informations de référence

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

Recommended Posts

Mock et stub avec minitest (utilisez RR, WebMock, MiniTest :: Mock)
Utiliser Webmock avec Rspec
Utiliser java avec MSYS et Cygwin
Utilisez JDBC avec Java et Scala.
Utilisez FacesContext comme maquette avec PowerMockito