The more I think about all the brilliant minds within the (growing) Ruby community, the more depressed I get. I mean, really: what are the other languages going to do? We’re hoggin’ all the genius.
Case in point: Mocha. It’s for mocking and stubbing, and it’s simple. Real simple. Ruby simple. Get it now:
$ sudo gem install mocha
or (in Rails)
$ script/plugin install svn://rubyforge.org/var/svn/mocha/trunk
I’ve used it to speed up my tests and whatnot. Nothing serious. But this week I’ve been waist deep in SOAP (woot!) and had a real need.
News and Letters
Here’s the deal: users can sign up for newsletters which are sent out weekly, daily, whenever. A separate system maintains the newsletters and their subscription lists. It also does the actual sending. You know, a spam cannon.
All the management stuff is done through SOAP. If a user clicks “Subscribe” on the site, we need to make a SOAP call to actually subscribe the user to that list. Same with unsubscribe. We also need to make a SOAP call to find which newsletters are available. Nothing stored locally.
I wrote a class, SpamCannon, which wraps the SOAP client. The only problem (besides, well, SOAP) is testing. I can’t test my class while it’s making real live SOAP calls: depending on an external resource is a huge no-no. What I need to do is (you guessed it) mock.
SpamCannon
Here’s the starts of my spam_cannon.rb.
require 'soap/wsdlDriver' require 'cgi' class SpamCannon class << self attr_accessor :wsdl, :client def newsletters @newsletters ||= client.getNewsletters end def client @client ||= SOAP::WSDLDriverFactory.new(wsdl).create_rpc_driver end end end SpamCannon.wsdl = 'http://soap.newsletters.website.com:9631/services/SCC?wsdl'
Now I can call SpamCannon.newsletters to get an array of available newsletters. In theory, at least. Better write a test.
The First Mock
The very first thing I’m going to do is deal with SpamCannon.client. See, it returns a SOAP instance based on the wsdl. I can’t have it doing that. Instead, I need client to return a mock. Easy.
SpamCannon.client = mock()
So what happens when I call SpamCannon.newsletters now? After all, that method tries to call client.getNewsletters and I’ve replaced client with a mock which probably knows nothing of camelcase or SOAP. Instead of finding out, I’m going to stub getNewsletter so it returns what I want it to return: fake newsletters.
All this lying. Oh well. I’m gonna create two fake newsletters: The Daily Rubyist and Gently Mockin. I’ll make them Structs, because I know my SOAP calls return similar Structs.
Object.const_set(:FakeNewsletter, Struct.new(:list_id, :title, :summary, :frequency)) unless defined? FakeNewsletter @letters = [] @letters << [ 'n313', 'The Daily Rubyist', "A daily recap of what's hot and what's not.", 'daily' ] @letters << [ 'n421', 'Gently Mockin', "A weekly reader on mocking and related stories.", 'weekly' ] @letters.map! { |s| FakeNewsletter.new(*s) }
First, I setup FakeNewsletter by, as I said, creating a Struct. (More on structs) I then set @letters up as an array to make it easier for me to see what I’m working with. Finally, I turn my arrays into real objects by passing all the elements of each to FakeNewsletter.new and overriding @letters with the instantiated objects.
The details aren’t important. What’s important is I now can do @letters.first.title and get The Daily Rubyist. I’ve got an array of nice, fake objects.
Next, more Mocha:
SpamCannon.client.stubs(:getNewsletters).returns(@letters)
Reads nicely, doesn’t it? I’m just saying that I want SpamCannon.client.getNewsletters to return @letters, as if I overrode the method myself.
Testing Your Functionality
We’re getting to the point. We don’t really care about the SOAP stuff—it’s out of our hands. We want to make sure the code we’re writing works, and test that. So let’s write some code.
Right now SpamCannon.newsletters returns an array. I want it to return a hash with keys corresponding to the list_ids of the newsletters. Makes life easier. Here are tests for that functionality:
def test_newsletters_returns_hash assert_equal Hash, SpamCannon.newsletters.class assert_equal 2, SpamCannon.newsletters.size end def test_newsletters_has_the_key assert_equal %w[n313 n421].sort, SpamCannon.newsletters.keys.sort end def test_newsletters_hash_is_properly_indexed assert_equal @letters.last, SpamCannon.newsletters[@letters.last.list_id] end
They all fail, but we can make them pass. Here’s my new SpamCannon.newsletters method:
def newsletters return @newsletters if @newsletters @newsletters = client.getNewsletters.inject({}) do |hash, letter| hash.merge(letter.list_id => letter) end end
Little o’ that and pass, pass, pass. We’re now testing our own code and using Mocha to ensure we have a controlled resource on which to build. If our code breaks, we know who to blame. (Java.)
Alright, don’t get too excited yet. We still need to let users subscribe to newsletters, which is kinda the point. I’m gonna write some simple wrapper methods that don’t do much and are somewhat hard to test—luckily Mocha makes it easy.
Expecting, Are We?
I need a way to create users in the remote SOAP database so they can be included in subscription lists. Here’s my simple wrapper method to do that:
def create_user(email, vitals, subscriptions = []) client.createUser(email, vitals, subscriptions) end
So what do I test? Do I just stub createUser and have it return true? Nay. Mocha has this neat concept called expectations which are, in sooth, assertions that ensure your method was called the way you wanted it called.
Here:
def test_create_user_works user = @users[:nic] SpamCannon.client.expects(:createUser).with(user.email, user.vitals, user.subscriptions).returns(true) assert SpamCannon.create_user(user.email, user.vitals, user.subscriptions) end
Ignore where the user is coming from for now and check out that SpamCannon.expects line. I’m saying that, in this test method, createUser needs to get called on client with the parameters I provide. It should then return true. If that method doesn’t get called in the way I describe, my test fails. Real cool. I can be sure SpamCannon.create_user is doing what I want it to be doing if this test passes.
Here’s what happens if I comment out the assert line and run my test:
1) Failure: test_create_user_works(SpamCannonTest) [./test/unit/spam_cannon_test.rb:41]: #<Mocha::Mock:0x1338512>.createUser('dr@nic.com', {:state => 'N/A', :age => 19}, ['n313', 'n421']) - expected calls: 1, actual calls: 0
Nice and informative. My SpamCannon class has a lot of simple methods like this and expected comes in handy all over the place.
Just The Beginning
If you wanna play, here are the finished class and test files:
They work standalone as long as you have the Mocha gem installed.
For more, Kevin Clark has written some greats on mocha in the past. Jay Fields, too.
Then there’s James Mead’s blog and his Mocha docs post, a good starting point. He’s one of the Mocha dudes, so definitely subscribe to his feed.
Oh, there’s a cheat sheet, too.
Sure beats monkey patching, yeah?
When is the “Err the Blog” book coming out so I can take it to bed and cuddle it?
Nic: You could print out all the posts, place them in a binder, and try cuddling with that. Might have some sharp edges, and it doesn’t have that new book smell, but sometimes you gotta make do.
I love you guys, thanks for enlightening the masses.
Err the… Book?
I don’t know if you noticed yet, or if I have this correct, but I believe that the returns() method actually acts the way it does when chained with stub(). Foo.expects(:bar).returns(:baz) will not verify that :baz was returns, but rather stub the method out to return that. It will still verify that it was called or not. So, then if I said assert_equal(:baz, Foo.bar) after the expectation, it would always pass, but not because the data would actually come out in that form in practice.
I just read (most of) Fowler’s “Mocks aren’t stubs” article and it highlights two different design styles that relate to all this which I found very interesting: interaction testing vs. state testing.
http://www.martinfowler.com/articles/mocksArentStubs.html
The point he makes about the way interaction testing (like mocha’s expectations) couple the tests to the implementation of what they are testing is worth extra attention. Possibly bad juju, as I have begun to note as I go mocha chrazy. I am thinking a balance is key, as usual in life.
Seth, what Chris describes in his article is mainly stubbing, and although he goes into mocking with expectations, he doesn’t really use it. I think this is the kind of balance you would need.
I can see how the interaction testing can be helpful, but I see how state testing is just easier, and almost safer when you are first designing your application.
For me at least, testing is just an extension of your spec doc, and state-based testing makes that easier, by defining the way things are going to be accessed and used, and not worrying about the implementation. This allows you to completly replace an algorithm or block of code for a better one, and all your tests will still pass, because teh end result is the same.
I feel like I just repeated what you and Fowler said, but ohh well.
I’m going to need to keep up with your blog, and Ruby more.
I love this stuff, and Ruby is exactly what I love, and not what I hate. I’ll go get myself a nice CS degree while I’m at it.
Chime in.