Jamie van Dyke is proficient in Ruby (and Rails). He teaches, he fabricates and is working for Boxed Up.
Building a Gem Using BDD
inscribed on 09 January, 2009
I wrote this article for the first edition of “The Rubyist”, and having left it a good set of months, I’m posting it here for your perusal.
The internet is full of tutorials and blogs on Rails, but lacking in the Ruby Gem building department, and as part of my work for Engine Yard over the last year I’ve been building internal tools packaged as Ruby Gem’s. I’d like to share my method’s with you now, and I hope it helps you building your own Gem’s.
We’re going to build a Gem that translates from American to British, it’s going to be simple, but it will demonstrate how to think about what you’re making and how to go about it. We’ll be testing with RSpec, for more details on the syntax of RSpec, please see their site.
If you’d like to skip ahead and see the entire application, I’ve put it on my github account.
Skeleton Structure
I use a gem called Mr Bones to generate the structure for my Gem’s, you can get it yourself using the gem command. On windows, omit the sudo command.
$ sudo gem install bones
I use bones to generate a skeleton structure for our gem. It creates Rake files that do lots of funky bits and pieces for us, as well as a good folder structure and a ready to roll ‘Britify’ module under the lib folder. Let’s create that now.
$ bones create britify
Mr Bones has now built us a structure, and all you have to do is edit the files Mr Bones told you to. The next step is to start building up the specs for our Gem, so let’s get cracking.
Start the Build Cycle
There is a specs/britify_spec.rb file already in place, but I prefer to structure my spec folder so that it automatically works with the autotest application. We won’t be using it in this tutorial, but bear in mind that I usually have it running so that I don’t need to re-run my tests constantly. I suggest you look into it on the ZenTest web site.
If we think about how we’re going to deal with the translations a little bit, it makes sense that we’ll need a Translation class to do the actual work. Now you’ll see exactly how BDD really works, as we build our first spec. Go ahead and create the following file, I’ve annotated it with comments to describe each piece. You’ll need to duplicate the exact folder structure for any file I describe in a code block, like below, use the first comment which tells you where to create it.
# spec/britify/translate/translate_spec.rb # $Id$ # Require the spec helper relative to this file require File.join(File.dirname(__FILE__), %w[ .. .. spec_helper]) # No need to type Britify:: before each call include Britify describe Translate do # All of our specs for Translate will go in here end # EOF
It’s pretty empty right now, but the basics are in place. In fact, if we run this spec it will fail which is exactly what we’d expect. Let’s get something in it first though.
A Translate class, in my opinion, would be instantiated without the need for any arguments. This seems like a good place to start for our specs. In between the describe block above, let’s elaborate on that thought.
# spec/britify/translate/translate_spec.rb it "should be instantiated without any arguments" do lambda { Translate.new( ) }.should_not raise_error lambda { Translate.new( "moo" ) }.should raise_error(ArgumentError) end
This seems reasonable enough. Running rake on the command line (to run our tests) tells me that I have an uninitialized constant Translate. Well of course I have that error, I haven’t created that class yet! I use this as my sanity check, now it’s time to make that spec pass.
# lib/britify/translate/translate.rb module Britify class Translate end end
Run your specs and you’ll get a successful pass. Brilliant. Further into the rabbit hole we go! I would expect our class to have a method that accepts an American sentence or word and returns the British version, I’d probably call it translate, but that’s too big a leap for my liking, I’ll tone it down to go in little steps.
# spec/britify/translate/translate_spec.rb it "should accept a string on a translate method, and return a string" do t = Translate.new t.translate("wonky").should be_instance_of(String) end
It seems like a strange test, because we’re merely saying that if I pass in a string that i should get one returned. We’re not saying that it should be a correct translation, but that’s the whole point. We want to go in small steps so the specs cover as much of our thinking as possible and basically describe our application logic. The pattern from here on is pretty similar, so apart from the explanation of my thought pattern I’ll whizz you through each file and change that we make as we go.
# lib/britify/translate/translate.rb def translate( string ) "wonky?" end
Yup, tests are passing again. Now, before I can implement a translation method I’ll be wanting somewhere to store my translations for looking up. I like the idea of YAML for this simple app, I’ll store it in data/translations.yml, and I’m thinking a simple hash of key value pairs like "American" => "British" will suffice for now.
It makes sense that we’ll want the Translate class to load this in straight away so they’re immediately available for translations.
# spec/britify/translate/translate_spec.rb it "should load the translations in from the data file on instantiation" do IO.should_receive(:read).and_return( "--- \nshut your gob: shut your mouth\nsnog: make out" ) t = Translate.new t.translations.should be_a_kind_of(Hash) t.translations.keys.size.should == 2 end
Notice the should_receive method there? This basically overrides the real functionality and fakes a response. The usual reason for this is that we don’t want to test the functionality of others classes and libraries, only our own. However, the real reason here is that as our translations grow the test will fail. So I fake a response in this spec and I’ll test the functionality more in other tests to ensure as we grow everything stays sane.
We need to make two changes to our Translate class for this to pass, the first is we need to require the YAML library at the top of the Translate class (above everything).
require 'yaml'
Secondly we need to implement the YAML loading functionality.
# lib/britify/translate/translate.rb def initialize translations_file = File.join(File.dirname(__FILE__), %w[ .. .. .. data translations.yml ]) @translations = YAML.load( IO.read(translations_file) ) end
We also need to expose the instance variable as a reader.
# lib/britify/translate/translate.rb attr_reader :translations
Okay, so far so good. Our specs are ready for our simple YAML store, and our tests are passing. How about a real translations file.
# data/translations.yml --- # American: British shut your mouth: shut your gob make out: snog not straight: wonky rubber boots: wellies
Now’s the time to start getting some real translation tests in place. I’ll adjust the spec for accepting a string to need a real translation.
# spec/britify/translate/translate_spec.rb it "should accept a string on a translate method, and return a string" do t = Translate.new t.translate("not straight").should be_instance_of(String) t.translate("not straight").should == "wonky" end
I’ve also noticed I’m repeating myself instantiating a Translate class, let’s dry that up. Place the following immediately after the describe Translate do line in our spec.
# spec/britify/translate/translate_spec.rb setup do @t = Translate.new end
Now change any reference of t.translate to @t.translate and get rid of the Translate.new calls too (except for the one you just added in the setup block).
Let’s get our specs passing again.
# lib/britify/translate/translate.rb def translate( string ) @translations[string.downcase] end
Summary
At this point we have a working (in a comic way) translation library, it needs a lot of work to make it stable though. Using your new found BDD cycle skills you could easily get in some more functionality and sanitize the input. Here’s some examples of what needs doing:
- Sanitize the input from non-alphabetic characters
- Create a command line interface for it
- Add more translations!
If you’d like to play around with the idea some more, a more rounded version (with the completed tasks above) is available on my github account:
Please do fork it and make changes, I’m open to patches. I hope you’ve enjoyed this view into my development mind and if you have any questions please email me.
Recent Comments