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.
Two problems
But there’s two problems with this test:
- Your
Spider
instance 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 FakeResponse
object.
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.