Let’s say you have a Ruby class that retrieves the contents of web pages, and you need to write a unit test for it…
class Spider attr_accessor :address, :path def get_response response = Net::HTTP.get_response(@address, @path) end def get_body get_response.body end end
You’ve tested the
get_response method, and now you need to test
get_body. You’re using MiniTest, a unit testing framework that comes standard with Ruby.
class TestSpider < MiniTest::Unit::TestCase def test_get_body spider = Spider.new spider.address = 'programming.oreilly.com' spider.path = '/2014/02/why-ruby-blocks-exist.html' assert spider.get_body == '<h1>Hi!' end end
You create a
Spider instance, assign it a page to retrieve, and at the end of the test, assert that the HTTP response matches your expected value. Simple enough.
But there’s two problems with this test:
Spiderinstance is making a real network request for the page, slowing the test and incurring network overhead for both you and the host you visit.
- The page could easily change on the remote side. You can use the current page contents in the test assertion, but it might need to be changed in the future.
In fact, the latter issue is causing your test to fail right now, because the real page doesn’t match the simplified HTML in your test.
Finished tests in 3.767214s, 0.2654 tests/s, 0.2654 assertions/s. 1) Failure: test_get_body(TestSpider) [-:26]: Failed assertion, no message given.
The output above also shows that this single test took nearly 4 seconds to complete while it waited for the HTTP response. This test is going to be run hundreds or thousands of times over your product’s life cycle. You want it to be as fast, efficient, and stable as possible. In its current state, it’s just not going to work.
A test double object, using Ruby’s “duck typing”
But we’re testing the
get_body method right now, not the
get_response method. Who says the
get_response method has to retrieve the real page? Maybe we could return some kind of fake response object – a test double.
In strongly-typed languages, that’s more difficult than it sounds. They’re expecting an object of a particular type – one that implements all of the methods the real object does. Only one method may actually get called on your double, but unless it implements all the others, you won’t be able to pass it to the method you’re testing. In such languages, it’s common to have test double classes with dozens of nearly-empty methods, just so your tests will compile. Complex third-party object mocking frameworks such as Moq (for C#) and jMock (for Java) have arisen to help with this problem, but you still have to install one and learn to use it.
Ruby makes it much easier on you. It follows the duck typing principle, as in “if it walks like a duck and quacks like a duck, I’m going to treat that object as if it were a duck”. Ruby doesn’t care what an object’s type is, it only cares that it has the necessary behavior. For purposes of this discussion, duck typing means that you can use any object to test the
get_body method, as long as it has a
body method of its own (because that’s the method that’s going to get called on it during the test).
... def get_body get_response.body end ...
How do we get a test double with a
body method? Well, there are Ruby libraries out there (such as the venerable RSpec) that will create doubles for you, but for an example this simple, we don’t need one. Ruby classes are so easy to create, that we can make a
FakeResponse class with a
body attribute in just three lines of code:
class FakeResponse attr_accessor :body end
When we create an instance of
FakeResponse, we can assign any expected value we need to the
body attribute, and get it back out again:
response = FakeResponse.new response.body = "<h1>Hi!</h1>" puts response.body # Prints "<h1>Hi!".
There it is – an object where you can call the
body method and get the return value we need. And thanks to duck typing, we can substitute it for the return value from
get_response, and the Ruby interpreter won’t complain.
Stubbing a method to return our fake response
But that brings us to our next problem:
get_response is called directly within
get_body. We need to make
get_response return a
FakeResponse instead of actually visiting the page and returning a real response – we need to stub out the method.
Fortunately, Ruby has another feature that can help us: singleton methods. A singleton method is a method that is defined on one single object – it doesn’t exist anywhere else.
my_string = "ruby is so cool" def my_string.sort_words self.split(' ').sort.join(' ') end puts my_string.sort_words # Prints "cool is ruby so" "don't have this method".sort_words # Error: undefined method
You can also use singleton methods to override an existing method on an object. Normally, the
length method on a
String object gives its length in characters, but we can override it, just for one object, to give its length in words:
my_string = "ruby is so cool" def my_string.length self.split(' ').length end puts my_string.length # Prints "4" puts "haven't overridden this method".length # Prints "30", like normal
Let’s use singleton methods to stub out the
get_response method on our
Spider instance. Currently, the method is making a real HTTP request, and returning a real response object. Let’s override
get_response, just on the instance in our test. We’ll have it skip making the network request, and return a
class TestSpider < MiniTest::Unit::TestCase def test_get_body spider = Spider.new spider.address = 'programming.oreilly.com' spider.path = '/2014/02/why-ruby-blocks-exist.html' def spider.get_response response = FakeResponse.new response.body = '<h1>Hi!' response end assert spider.get_body == '<h1>Hi!' end end
Thanks to duck typing, the
get_body method won’t care that
get_response is returning a
FakeResponse, as long as it can call a
body method on it. And the value returned by
body exactly matches the expectation in our test.
def get_body get_response.body end
Let’s try running it:
Finished tests in 0.000660s, 1515.1515 tests/s, 1515.1515 assertions/s. 1 tests, 1 assertions, 0 failures, 0 errors, 0 skips
It’s passing, and it’s much faster (far less than a second)! No unnecessary network requests, and a short, predictable value to test for. We didn’t even have to use any fancy mock object libraries.
We all want to write more, better, faster-running tests. That’s why Ruby’s duck typing and singleton methods are so great – they make writing tests easy.