Learning Ruby with TDD
Wednesday, 9 November 2011 00:00 UTC
I was starting to have a look at the book from Kent Beck (Test Driven Development: by Example), shame on me that I didn't yet find the time to read it, and I thought I could use the occasion to get the basics of Ruby. Caution: I have no previous knowledge of Ruby. On one side, this means that I will write particularly bad code in ruby. On the other side, it is a nice opportunity to put TDD to good use and see if I can refactor some code as soon as I learn the "Ruby way" of doing stuff. The idea is to go through the basic Money example from Kent's book and just use Ruby for the code. Luckily Ruby comes with a basic unit testing framework, and I think it's more than enough to start. I will be using Textmate to code and run the tests and ruby 1.8.6 on OS X.
Let's get started!A short summary for those who didn't read Kent's book: we are trying to define a class which should be able to perform different operations on money with different currencies. We're going test first, so the first piece of code we are going to write in Ruby is a unit test! Yay! To be able to access the unit testing classes we are going to need the file test/unit.rb. There are two ways to do this: one is the require method, and the other is the include method. To keep it short, we are going to use the require method because it just runs the file, but for a deeper analysis of the differences you can have a look here. Just a tip: include is a "false friend" for developers with C/C++/C# backgrounds, so be careful. Now that we've got that out of the way, we can write the test class with the first test (multiplication). I chose to write both tests and code in a file named Money.rb. Let's see how that looks in Ruby:
Ok that looks pretty easy: we defined a class MoneyTests which inherits from Test::Unit::TestCase. The class has a method test_multiplication that checks if a dollar multiplied by 2 is 10 dollars. Let's run the tests simply by going into a terminal and writing "ruby Money.rb", or from inside TextMate which I personally prefer (shortcut CMD + R), if only for the red/green colors jumping to my face at a key press.
require 'test/unit' class MoneyTests < Test::Unit::TestCase def test_multiplication five = Dollar.new(5) five.times(2) assert_equal(10, five.amount) end end
Loaded suite /Users/filippo/Documents/RubyTests/Money Started E Finished in 0.00031 seconds. 1) Error: test_multiplication(MoneyTests): NameError: uninitialized constant MoneyTests::Dollar method test_multiplication in Money.rb at line 7 1 tests, 0 assertions, 0 failures, 1 errorsIt looks like we have some coding to do! We have to define a Dollar class with a constructor taking an integer as input, define a method times and a field amount. I'll give it a try:
The class is defined but the test is failing with the following error:
class Dollar attr_reader :amount def initialize(amount) @amount = amount end def times(multiplicator) end end
Loaded suite /Users/filippo/Documents/RubyTests/Money Started F Finished in 0.007974 seconds. 1) Failure: test_multiplication:18 <10> expected but was <5>. 1 tests, 1 assertions, 1 failures, 0 errorsIt turns out we have to implement the times method (no way!). I feel pretty comfortable in implementing an integer multiplication, so I'll go on and just implement a basic behavior without needing to fake the result:
We can re-run the tests now and we get our first successful assert in ruby!
def times(multiplier) @amount = amount * multiplier end
Loaded suite /Users/filippo/Documents/RubyTests/Money Started . Finished in 0.000259 seconds. 1 tests, 1 assertions, 0 failures, 0 errorsComforting, isn't it? Please take a moment to enjoy the cozy illusion of a passing test. Back? Great, because now it's time to change our multiplication API. What? Already? Yes, why, it's software we're writing, silly. In fact, as Kent notes in the book, it is a bit awkward to work with a field. Besides, wouldn't it be nice to be able to write the following?
Why do we wantto do that? The devil lies in the details...look at how we named the variable containing the value of 5 dollars: five. We see it as immutable. We want it to be immutable. This in turn means that we have to return a new Dollar as the result of the multiplication, which would make the test code look like this:
def test_multiplication five = Dollar.new(5) five.times(2) assert_equal(10, five.amount) five.times(3) assert_equal(15, five.amount) end
Does it look good enough? We modified the test so that the API is a bit more predictable and easy to use, and we have to change the times method to return a new Dollar instance like this:
def test_multiplication five = Dollar.new(5) product = five.times(2) assert_equal(10, product.amount) product = five.times(3) assert_equal(15, product.amount) end
Running the test we see that it is green again! The next step is testing for equality:
def times(multiplier) Dollar.new(self.amount * multiplier) end
As a side note, it looks like there are three equality operators in Ruby: "==", "eql?" and "equal?". According to this page the equivalent for the Java equals() is the == operator, so I've decided to use it in the test. As the test is failing, we override the implementation of the == operator to return a comparison of the amount fields in the corresponding classes:
def test_equality assert(Dollar.new(5) == Dollar.new(5)) end
This implementation makes our test pass, which in turn should make us happy! Now that we implemented the equality operator, we can make our multiplication test even more clear:
def ==(dol) self.amount == dol.amount end
Actually, we can also get rid of product:
def test_multiplication five = Dollar.new(5) product = five.times(2) assert_equal(Dollar.new(10), product) product = five.times(3) assert_equal(Dollar.new(15), product) end
At this point it is time to get rid of the amount field, because it is only used by Dollar. This one is a bit more tricky and it took me a bit longer to port to Ruby. The new Dollar class looks like this:
def test_multiplication five = Dollar.new(5) assert_equal(Dollar.new(10), five.times(2)) assert_equal(Dollar.new(15), five.times(3)) end
amount is now an instance variable, and to access the instance variable from the other dollar object anotherDollar I had to use the method instance_variable_get. So I learned that you can actually access any instance variable of an object with this method. Nifty!
class Dollar def initialize(amount) @amount = amount end def times(multiplier) Dollar.new(@amount * multiplier) end def ==(anotherDollar) @amount == anotherDollar.instance_variable_get("@amount") end end
blog comments powered by Disqus