Ruby and RSpec: Powerful Languages Allow Simpler Frameworks

Recently I was doing a simple kata – the Roman Numeral kata – to practice my Ruby and RSpec skills. (The Roman Numeral kata is to build an algorithm test-first that converts a number into its Roman numeral equivalent. For example, 1 to i, 7 to vii, 10 to x, etc. The problem is intended to be simple so that you can focus on the process.) I had the following RSpec code:

require 'roman_numeral'

describe RomanNumeral do
  # specify is just an alias for it. specify reads better in this case
  specify '1 should be i' do
    numeral = RomanNumeral.new(1)
    numeral.to_s.should == 'i'
  end
end

I go through the Red/Green/Refactor cycle and get this first spec working. (I won’t show the production code as it’s not that interesting.) I move onto the second spec.

require 'roman_numeral'

describe RomanNumeral do
  specify '1 should be i' do
    numeral = RomanNumeral.new(1)
    numeral.to_s.should == 'i'
  end

  specify '2 should be ii' do
    numeral = RomanNumeral.new(2)
    numeral.to_s.should == 'ii'
  end
end

Red/Green/Refactor and all is good. But I’m seeing some repetition in my specs and it’s only going to get worse. The specs are almost identical except for the data. So I start hunting through the RSpec docs for something akin to NUnit’s [TestCase] or xUnit’s [Theory]. I find nothing. Maybe, like MSpec, RSpec wasn’t designed to do this sort of data-driven testing.

Undaunted I turn to Google and stumble upon this StackOverflow question asking exactly the same question that I had. Matt Di Pasquale, the OP (original poster), found his own answer and that answer was fascinating! No, RSpec doesn’t have a syntax for test cases because it doesn’t need one. Use the Ruby, Luke!

require 'roman_numeral'

describe RomanNumeral do
  cases = {
    1 => 'i',
    2 => 'ii'
  }

  cases.each do |k, v|
    specify "#{k} should print as #{v}" do
      numeral = RomanNumeral.new(k)
      numeral.to_s.should == v
    end
  end
end

For those not as well-versed in Ruby, “cases” is simply a variable that contains a hash. I then iterate over the key/value pairs and define my specifications. It’s that simple. I didn’t need any special attributes or framework support from RSpec. I didn’t need to learn the inner workings of RSpec to write my own custom extension or attributes. I simply wrote some Ruby code. Let that sink in for a second. I simply wrote some Ruby code.

Now think about that a bit more. I’m writing regular Ruby code to implement the notion of test cases. Rather than defining test cases, I could have used very similar code to define a benchmark that verifies an algorithm scales linearly with number of input elements. Or I could have fuzz tested an external API for my application. The possibilities are only limited by my imagination and don’t require me to gain an ever deeper understanding of my testing/specing framework to implement.

For someone who cut his teeth on C, C++, and C# (with some JavaScript thrown in for good measure) and is used to learning (or building) frameworks to solve problems, the notion that you can just write plain old code to solve these types of meta problems is eye-opening. It is one of those ah-ha moments that matures you as a developer. Not every problem needs a framework to solve it. Sometimes it just requires a little code.

About James Kovacs

James Kovacs is a Technical Evangelist for JetBrains. He is passionate in sharing his knowledge about OO, SOLID, TDD/BDD, testing, object-relational mapping, dependency injection, refactoring, continuous integration, and related techniques. He blogs on CodeBetter.com as well as his own blog, is a technical contributor for Pluralsight, writes articles for MSDN Magazine and CoDe Magazine, and is a frequent speaker at conferences and user groups. He is the creator of psake, a PowerShell-based build automation tool, intended to save developers from XML Hell. James is the Ruby Track Chair for DevTeach, one of Canada’s largest independent developer conferences. He received his Bachelors degree from the University of Toronto and his Masters degree from Harvard University.
This entry was posted in Ruby. Bookmark the permalink. Follow any comments here with the RSS feed for this post.
  • Anonymous

    It is a function of the way idiomatic ruby code and tools are written. You absolutely could build a c# test framework, but you would probably be bending over backwards to design it in a way just to support this specific scenario, as opposed to getting it for free.

  • Dathan Bennett

    That appears to me to be a function of RSpec, though, not Ruby.  I don’t see any reason why I couldn’t create a testing framework for C# that mimics the “specify” behavior used above.  I can’t see any way to make it syntactically as clean — but certainly doable. 

  • http://jameskovacs.com James Kovacs

    It’s one of the truly hard things about teaching and writing – being able to put yourself in the shoes of the educated, but unknowing individual. What is obvious to you as a practictioner might not be immediately obvious to others. I’ll try to provide more background next time for those not familiar with Ruby. I appreciate your feedback.

  • http://jameskovacs.com James Kovacs

    Thanks for sharing the JavaScript version. Once again it demonstrates that JavaScript is a true dynamic language and has little to do with namesake other than clunky syntax. :)

  • Tudor

    In this case you should explain how that test is executed and the meaning of those Ruby keywords, otherwise most people will not  understand the idea of the entire post, and just remain (like me) with the impression that Ruby is just another weird language with some strange constructs..

  • Anonymous

    Yup, this looks exactly like some JavaScript code we have in our test suite. Here’s the example in JavaScript using Mocha + a standard should-style assertion library (like Chai).

    “`
    var romanNumeral = require(“roman-numeral”);

    describe(“RomanNumeral”, function () {
        cases = { 1: “i”, 2: “ii” };
       
        Object.keys(cases).forEach(function (k) {
            var v = cases[k];
           
            it(k + ” should print as ” + v, function () {
                var numeral = romanNumeral.new(k);
                numeral.should.equal(v);
            });
        });
    });
    “`

    Notably looping through hashes is pretty ugly in JavaScript [1]. And Mocha doesn’t have a `specify` synonym [2]. Finally all the `function () { }` wrappers are slightly uglier than `do`/`end`, I think.

    Things really shine in CoffeeScript though, which is how I write all my tests (despite writing the code being tested in JavaScript):

    “`
    romanNumeral = require “roman-numeral”

    describe “RomanNumeral”, ->
        cases =
            1: “i”
            2: “ii”
       
        for k, v of cases
            it “#{k} should print as #{v}”, ->
                numeral = romanNumeral.new(k)
                numeral.should.equal(v)
    “`

    [1] For now; there are some ES6 `for … of` proposals that should help. `for ([k, v] of pairs(cases))` or something similar.
    [2] For now: https://github.com/visionmedia/mocha/issues/396

  • http://jameskovacs.com James Kovacs

    There is a subtle difference between the two, which Joshua notes below. He is spot on.

    I would like to say that my post was not meant to bash C#. I use C# a lot and it excels at many things. That said, it’s not always the best tool for the job. By learning other languages, you see how other languages solve the same problem – sometimes more elegantly, sometimes less. Either way you learn.

  • http://jameskovacs.com James Kovacs

     Exactly right, Josh. I should have noted that subtle difference. Multiple logical assertions in one test is definitely a code smell that should be avoided. The Ruby code avoids that by dynamically defining the specs.

  • Anonymous

    Not the same. In James’ example, he has 2 tests cases that can fail independently (as you would get with NUnit’s TestCase feature). In your example, you have a single test case with multiple assertions. If both scenarios are broken, you still only get a single failing test.

  • Matthew Leibowitz

    I felt I needed to comment. So I’ll introduce myself: I believe good code is C# code. There may be some exceptions, but I can’t/don’t  see them ;)

    But back to the reason:
    I created a snipped in C# that does exactly what you believe only Ruby can do:

    var cases = new Dictionary
                        {
                            {1, “i”},
                            {2, “ii”},
                            {3, “iii”}
                        };
    foreach (var c in cases)
    {
        Console.WriteLine(“{0} should print as {1}”, c.Key, c.Value);
        var numeral = new RomanNumeral(c.Key);
        Assert.AreEqual(numeral.ToString(), c.Value);
    }

    Go C#!

    Very simple, very neat, very the same!