No Network Please, I’m Testing

It’s good practice for your automated testing not to require a network connection to an external service to run. At the very least, using an external service is going to slow down your tests (and it could have other consequences).

One solution is to stub out or mock calls to external services, but it’s easy to forget to do that. For instance, let’s say we using HTTParty to POST data to an external website.

class FooClient
def self.send_data(value)
HTTParty.post(url, body: { value: value })
end
end

And here’s a test for it (using Minitest Spec and Mocha but the principle applies to all testing frameworks).

describe FooClient do
before do
HTTParty.expects(:post).once.returns(16)
end
  it "sends data" do
FooClient.send_data(4).must_equal 16
end
end

But what if you forget to stub out the post? As your code gets more complicated it’s not always so obvious that an external call is being made.

Enter the NoNetwork module:

# test/test_helper.rb
require 'support/no_network'
# test/support/no_network.rb
module NoNetwork
def before_setup
HTTParty.expects(:post).at_least(0).throws("DON'T USE THE NETWORK IN TEST. Please stub HTTParty.post")
end
end
class Minitest::Test
include NoNetwork
end

Now, if you forget to stub out HTTParty the earlier stub in the NoNetwork module will throw an error telling you about it.

Extending it to other services

You could add code to do the same thing for other services. Perhaps you’re using Stripe and want to isolate that too. Perhaps another part of your code uses Net::HTTP. You’ll probably want to stub out other methods on HTTParty. Let’s DRY this up and make it easier to add more checks:

module NoNetwork
  def before_setup
block_class_methods(
Stripe::Customer => [:create],
Stripe::Charge => [:create],
Stripe::Refund => [:create],
HTTParty => [:get, :put, :patch, :post, :delete]
)
block_instance_methods(
Net::HTTP => [:start, :request_get, :request_head, :request_post]
)
end
  def expect_and_throw(obj, method, description)
obj.
expects(method).
at_least(0).
throws("DON'T USE THE NETWORK IN TEST. Please stub #{description}")
end
  def block_class_methods(list)
list.each do |obj, methods|
Array(methods).each do |method|
expect_and_throw obj, method, "#{obj}.#{method}"
end
end
end
  def block_instance_methods(list)
list.each do |obj, methods|
Array(methods).each do |method|
expect_and_throw obj.any_instance, method, "#{obj}##{method}"
end
end
end
end
class Minitest::Test
include NoNetwork
end

Every time you add a new external service then you add it to this module and your whole test suite will then tell you if you’ve omitted to stub out a call.

And it’s a good idea to occasionally run your tests without being connected to the Internet just to check you’ve listed everything you use in the NoNetwork module.

You can see the code for this article here with each step as a separate commit.