[Ruby] Mock and stub with minitest (use RR, WebMock, MiniTest :: Mock)

14 minute read

About this article

At work, I use minitest as a rails testing framework. And in the rails project, when mocking and stubing, gems such as RR and WebMock and MiniTest :: Mock, which is a standard minitest mock, are used.
When writing tests, I was often confused about how to write mock and stubs.
This article describes what a test double is, what is the difference between a mock and a stub, and how to use each of RR, WebMock, and MiniTest :: Mock.

table of contents

-What is test double (mock, stub)

What is a test double (mock, stub)?

First of all, what is a test double? Also, what is the difference between mock and stub?

–A test double is a substitute for a component that the test target depends on in software testing (double means a substitute, a shadow warrior).
–Mock and stub are a kind of test double

5 variations of test double

There are five variations of test doubles, according to the book xUnit Test Patterns (http://xunitpatterns.com).
I’ve often confused stubs with mock, but ** stubs replace dependent components, and mock verifies that the output from the code under test is as expected **, I understood that.

  1. Test stub
    –Used to replace the actual component on which the code under test depends
    –Set to return a predetermined value when calling during testing

  2. Test spy
    –Capture the indirect output when the code under test is executed and save it for verification by later testing
    –Stub to record information based on the call

  3. Mock object
    –Object used to validate indirect output from the code under test when the code under test is executed
    –Emphasis is placed on verification of indirect output
    –You can verify that the expected call was made (what argument was called, etc.)

  4. Fake object
    –Objects that replace dependent components in the code under test
    –Implements the same functionality as the dependent component, but in a simpler way
    –A common reason to use fake is that the actual dependent components are not yet available, are too slow, or cannot be used in a test environment due to side effects.

  5. Dummy object
    –Use a dummy object if you need an object as a parameter in the method signature of the code under test (if neither the test nor the code under test cares about this object)

reference
Test Double / xUnit Patterns.com
Wiki test double
Differences in automated test stub spy mock

RR
RR is a Ruby test double framework gem.
The reading is said to be’Double Ruby’.
Since RR has an adapter, it seems that it can be integrated with test frameworks such as RSpec, Test :: Unit, MiniTest / MiniSpec.

GitHub:https://github.com/rr/rr
Official: http://rr.github.io/rr/

RR is a test double framework that features a rich selection of double techniques and a terse syntax.
RR is a test double framework that features a wealth of double techniques and concise syntax.

How to use RR

RR implements mock, stub, proxy, and spy.
It is as the sample of RR GitHub page, but you can write it like this.

stub

You can stub (replace the actual call) with stub.

#Stub a method that returns nothing
stub(object).foo
stub(MyClass).foo

#A stub method that always returns a value
stub(object).foo { 'bar' }
stub(MyClass).foo { 'bar' }

#A stub method that returns a value when called with a specific argument
stub(object).foo(1, 2) { 'bar' }
stub(MyClass).foo(1, 2) { 'bar' }

See the stub page for details.

mock

With mock, you can create a mock that verifies that the expected call is made.

#Expect the method to be called
#Expect the foo method of object to be called
mock(object).foo
mock(MyClass).foo

#Create an expected value in the method and stub to always return the specified value
#object's foo method'bar'Expect to return
mock(object).foo { 'bar' }
mock(MyClass).foo { 'bar' }

#Create an expected value for a method with a specific argument and create a stub to return it
#object's foo method has argument 1,Called by 2'bar'Expect to return
mock(object).foo(1, 2) { 'bar' }
mock(MyClass).foo(1, 2) { 'bar' }

See the mock page for details.

spy

It seems that you can write a spy (a stub that records the called information) by combining stub with the description of ʻassert_received and ʻexpect (xxx) .to have_received.
(The official GitHub had a way to write it in Test :: Unit and Rspec, but it didn’t show how to write it in minitest.)

# RSpec
stub(object).foo
expect(object).to have_received.foo

# Test::Unit
stub(object).foo
assert_received(object) {|o| o.foo }

Proxy

It seems that using proxy allows you to create stubs and mock that intercept and set new return values without overriding the method completely.

#Intercept existing methods without overriding them completely
#Get a new return value from an existing value
stub.proxy(object).foo {|str| str.upcase }
stub.proxy(MyClass).foo {|str| str.upcase }

#In addition to what you're doing in the example above, create more expected value mock
mock.proxy(object).foo {|str| str.upcase }
mock.proxy(MyClass).foo {|str| str.upcase }

#Intercept a new method in the class and define a double in the return value
stub.proxy(MyClass).new {|obj| stub(obj).foo; obj }

#In addition to what we are doing in the example above.Create a mock of expected value in new
mock.proxy(MyClass).new {|obj| stub(obj).foo; obj }

For details, see mock.proxy, stub.proxy /blob/master/doc/03_api_overview.md#stubproxy) See page.

Instance of class

With ʻany_instance_of, you can stub or mock methods when instantiating. You can also use stub.proxy` to access the instance itself.

#Stub methods when creating an instance of MyClass
any_instance_of(MyClass) do |klass|
  stub(klass).foo { 'bar' }
end

#Another way to make the instance itself accessible
# MyClass.Stubing new instance obj
stub.proxy(MyClass).new do |obj|
  stub(obj).foo { 'bar' }
end

See the #any_instance_of page for details.

Pure mock objects

If you want to use the object only for mocking, you can do it by creating an empty object.

mock(my_mock_object = Object.new).hello

You can also use mock! as a shortcut.

#empty#Create a new mock object with a hello method and get that mock
#Mock object#Can be obtained with the subject method
my_mock_object = mock!.hello.subject

#dont_allow
#dont_allow is the opposite of # mock and sets the expectation that doubles will never be called. If the double is actually called, you will get a TimesCalledError.

dont_allow(User).find('42')
User.find('42') # raises a TimesCalledError

Other

It seems that RR uses # method_missing to set the expected value of the method. This eliminates the need to use the #should_receive and #expects methods.
Also, it seems that there is no need to use the # with method to set the expected value of the argument. (You can use it if you want)

mock(my_object).hello('bob', 'jane')
mock(my_object).hello.with('bob', 'jane')  #With is attached, but the same as above

RR supports using blocks to set the return value. (You can use # returns if you like)

mock(my_object).hello('bob', 'jane') { 'Hello Bob and Jane' }
mock(my_object).hello('bob', 'jane').returns('Hello Bob and Jane')  #Same as above with returns

You can adjust the expected number of mock calls with the #times, # at_least, # at_most, and # any_times methods. You can expect # with_any_args to allow calls with any arguments, # with_no_args to expect calls without arguments, and # never to expect no method to be called.
See the API overview (https://github.com/rr/rr/blob/master/doc/03_api_overview.md) for more information.

WebMock
WebMock is a gem for setting HTTP request stubs and mock in Ruby.
Is the difference from RR the part that specializes in HTTP requests?

GitHub:https://github.com/bblimke/webmock

Library for stubbing and setting expectations on HTTP requests in Ruby.

The following are provided as Functions.

–Stubing HTTP requests at the lib level of low-level http clients (no test changes required when changing HTTP libraries)
–Expected value setting and validation for HTTP requests
–Matching requests based on method, URI, header, body
–Smart matching of the same URI in different representations (encoded and unencoded)
–Smart matching of the same header in different expressions
–Support for Test :: Unit, RSpec, minitest

How to use WebMock

Here’s an excerpt from the sample WebMock GitHub Page.

stub

You can stub a request with stub_request.

Uri-only stub request and default response

stub_request(:any, "www.example.com")    #Stub (use any)
Net::HTTP.get("www.example.com", "/")    # ===> Success

Stub Requests Based on Method, URI, Body, Header (https://github.com/bblimke/webmock#stubbing-requests-based-on-method-uri-body-and-headers)

#stub
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

Match request body with hash (https://github.com/bblimke/webmock#matching-request-body-against-a-hash-body-can-be-url-encoded-json-or-xml)

You can match the request body with a hash when the body is URL-Encode, JSON, or XML.

#stub
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

You can use hash_including to match a partial hash with a request body.

#body hash_Match with partial hash including
#Can be collated even if all bodies do not match
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

Match Query Parameters (https://github.com/bblimke/webmock#matching-query-params-using-hash)

You can match query parameters with hashes.

#stub
stub_request(:get, "www.example.com").with(query: {"a" => ["b", "c"]})

RestClient.get("http://www.example.com/?a[]=b&a[]=c")    # ===> Success

As with the body, hash_including can be used to match partial hashes and query parameters.

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

You can use hash_excluding to match a state that is not included in the query parameter.

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 that returns a custom response

You can set a stub that returns a custom response with to_return.

#stub
stub_request(:any, "www.example.com").
  to_return(body: "abc", status: 200,
    headers: { 'Content-Length' => 3 })

Net::HTTP.get("www.example.com", '/')    # ===> "abc"

Raise errors

#Raise the exception declared in the class
stub_request(:any, 'www.example.net').to_raise(StandardError)
RestClient.post('www.example.net', 'abc')    # ===> StandardError

#Raise of exception instance
stub_request(:any, 'www.example.net').to_raise(StandardError.new("some error"))

#Raise an exception with an exception message
stub_request(:any, 'www.example.net').to_raise("some error")

You can also raise Timeout Exceptions (https://github.com/bblimke/webmock#raising-timeout-errors) with to_timeout.

stub_request(:any, 'www.example.net').to_timeout

RestClient.post('www.example.net', 'abc')    # ===> RestClient::RequestTimeout

Multiple different responses to repeated requests

When the request is repeated, it can return several different responses.
Also, connect to_return, to_raise, and to_timeout with then to return multiple responses -to_raise-or-to_timeout-declarations), use times to specify the number of times to return a response You can also.

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"

#After all responses have been used, the last response is returned indefinitely
Net::HTTP.get('www.example.com', '/')    # ===> "def\n"

Allow or disable real requests to the network (https://github.com/bblimke/webmock#real-requests-to-network-can-be-allowed-or-disabled)

You can allow requests to the actual network with WebMock.allow_net_connect!. It can also be disabled with WebMock.disable_net_connect!.
You can also Allow specific requests while disabling external requests I will.

#Allow requests to the actual network
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.+/

#Disable requests to the actual network
WebMock.disable_net_connect!

Net::HTTP.get('www.something.com', '/')    # ===> Failure

There are many other ways you can stub. See the Stubbing page for sample code for other uses.

Expected value setting (mock)

WebMock’s GitHub page has Test :: Unit and How to set expected value in RSpec, but there was no description about minitest.
It seems that minitest can be written in the same way as Test :: Unit (Reference).

Test::Unit/minitest

Use ʻassert_requested and ʻ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" }

To set the expected value using a stub, write as follows.

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
Write a combination of ʻexpect and 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'})

You can also write the following by combining ʻa_request and 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

To set the expected value using a stub, write as follows.

stub = stub_request(:get, "www.example.com")
# ... make requests ...
expect(stub).to have_been_requested

See the Expected Value Settings (https://github.com/bblimke/webmock#setting-expectations) page for more information.

Other

Reset all current stubs and request history with WebMock.reset!, or WebMock.reset_executed_requests" ! can only reset the counter for requests made.
At WebMock.disable! And WebMock.enable! You can disable or enable WebMock, or enable only some http client adapters.
For other features, see WebMock GitHub Page Sample Code (https://github.com/bblimke/webmock#examples).

MiniTest::Mock
Finally, MiniTest :: Mock is a framework for mock objects included in minitest.

Official documentation: http://docs.seattlerb.org/minitest/Minitest/Mock.html

A simple and clean mock object framework.
All mock objects are an instance of Mock.
(A simple and clean mock object framework. All mock objects are instances of MiniTest :: Mock.)

How to use MiniTest :: Mock

stub

Stubing an object stub is an object extension of Minitest :: Mock.
Stubs are only valid inside the block, and the stubs are cleaned up at the end of the block. Also, the method name must exist before it can be stubed.
The stub_any_instance method allows you to create method stubs on an instance of a class. You can use it by installing the gem of minitest-stub_any_instance_of.

–stub: Stub the method of the object
–stub_any_instance_of: Stub the instance method of the class

This is a sample code of stub.

require 'minitest/autorun'

#The class to stub
class Hello
  def say
    'Hello!'
  end
end

hello = Hello.new
#The say method of the hello object'Hello, this is from stub!'Stub to return
hello.stub(:say, 'Hello, this is from stub!') do
  hello.say  #==> "Hello, this is from stub!"
end
#Stubs are disabled when you exit the block
hello.say  #==> "Hello!"

Using stub_any_instance, you can write an instance method stub as follows: It seems that this is more useful when writing instance method stubs.

require 'minitest/autorun'
require 'minitest/stub_any_instance'  # minitest-stub_any_instance_of gem is also needed

#The class to stub
class Hello
  def say
    'Hello!'
  end
end

#The say method of any instance of the Hello class'Hello, this is from stub!'Stub to return
Hello.stub_any_instance(:say, 'Hello, this is from stub!') do
  Hello.new.say  #==> "Hello, this is from stub!"
end
#Stubs are disabled when you exit the block
Hello.new.say  #==> "Hello!"

mock

expect method

`expect(name, retval, args = [], &blk)
Expects the method name to be called, optionally with arguments (args) or blocks (blk), and a return value (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

The arguments are compared to the expected arguments using the’===’ operator, which reduces the specific expected value. (Compare by is it included?)

require 'minitest/autorun'

# users_any_Returns true if the string method is contained in a String
@mock.expect(:uses_any_string, true, [String])
@mock.uses_any_string("foo") # => true
@mock.verify  # =>true (becomes true because the mock was called as expected)

@mock.expect(:uses_one_string, true, ["foo"])
@mock.uses_one_string("bar") # =>raises MockExpectationError (because the mock wasn't called as expected)

If the method is called multiple times, specify a new expected value for each. These are used in the order they are defined.

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"

verify method

Make sure all methods are called as expected. Returns true if called as expected. Raises a MockExpectationError if the mock object is not called as expected.

See the MiniTest :: Mock page for more information.

Finally

For both RR and WebMock, the official documentation has a good sample of how to use it, so it seems good to read it. Since the amount of information in MiniTest :: Mock was small, I thought it would be easier to imagine if I checked the movement of mock and stub with ʻirb and rails c. (At runtime, require’minitest / autorun’` is required.)

Reference information

RR / GitHub
RR page
WebMock / GitHub
MiniTest::Mock
MiniTest stub
minitest-stub_any_instance
Mock, Stub Study Group (ruby)
Differences in automated test stub spy mock
Test Double / xUnit Patterns.com
Using stub and mock with minitest
Wiki test double

xUnit Test Pattern
Looking at the variations of the test double, here is xUnit Test Patterns: Refactoring Test Code books have appeared frequently.
It seems that only the English version has been published, but I was able to confirm the content on the Web (in English).
http://xunitpatterns.com