Thursday, 8 November 2012

Overriding equality and Test Driven Development

Ruby has, at its root, an Object. Methods available in Object are available to every class because every class in Ruby inherits from Object somewhere in its own class hierarchy. Of course, you can override methods in subclasses, changing the functionality of a root method.

You might stumble on to this idea if you work through Test Driven Development By Example by Kent Beck, translating the Java code into Ruby as you go. At some point pretty early on, he overrides the equality method on the Currency class to better test if two instances are equal. I'm going to do the same here, working with Instruments instead of Currency.

Equality

Equality in Ruby can be expressed using any of the following three methods

object == other
equal?(other)
eql?(other)

These methods are defined on the base Object. The default implementation of equality will only return true if both objects are exactly the same. The interesting thing is that although these three methods start out functioning the same, the documentation does note the difference between them:

Equality—At the Object level, == returns true only if obj and other are the same object. Typically, this method is overridden in descendant classes to provide class-specific meaning. Unlike ==, the equal? method should never be overridden by subclasses: it is used to determine object identity (that is, a.equal?(b) iff a is the same object as b). The eql? method returns true if obj and anObject have the same value. Used by Hash to test members for equality.

So we are encouraged to override == to test equality of our own classes and objects.

Instruments in irb

Let's put together a simple implementation to see how this works. Fire this up in irb:

class Instrument

  def play
    puts 'lovely sound'
  end

end

guitar = Instrument.new
=> #<Instrument:0x2a1b9d8>
another_guitar = Instrument.new
=> #<Instrument:0x2940c70>
guitar == another_guitar
=> false
guitar.equal?(another_guitar)
=> false
guitar.eql?(another_guitar)
=> false

You get the idea. The two guitars are not equal. Everything functions as per the documentation. Just for completeness, Ruby considers an object to be the same when it is *exactly* the same - keep typing:

third_guitar = guitar
=> #<Instrument:0x2a1b9d8>
third_guitar == guitar
=> true

That makes sense and behaves as per the documentation. Equality tests that the "actual" objects are equal. This is the equivalent of saying, in the real world, no two guitars are the same - even if they are both, for example, Ibanez Jems (see what I did there? ruby gems, guitar jems) with the Floral pattern that came off he production line one after the other.

Okay, the pedantic among you will argue that they won't be the same, the floral pattern will probably be a bit different - that was the point of Jems right? The tone might be a little different, the feel will be different - minutely different, but different none the less. Buuuuuut, I would argue, to all intents and purposes, these two fictional guitars are the same. They will have the same price tag, they will play pretty much identically (in double blind playing trials), they have the same number of pickups, same number of frets, same volume and tone potentiometer, same pickup-selector switch. They both have the same monkey handle, they will both produce the same note when tuned to A440.

The same

Given the above, for our example, we are going to say they *are* the same. In fact, to make it even simpler to show the code, I'm going specify that any 'guitar' is the same as any other 'guitar' (like Bruce Lee said "A kick is a kick, a punch is a punch").

To represent this in Ruby, we have to override object equality and put in its place, our own code to compare two instruments. That sounds a little scary to just bash out and I heard the word "specify" a few sentences ago, so I'm going to wire up some tests using the awesome minitest (last seen integrating into Rails) to help us code this and see where we get to.

Using various asserts included in our testing framework, we can test equality of objects because things like assert_equal actually just take the two objects and call the "==" method on those objects. Climb out of irb and create yourself a new file - I'm calling mine minstrel.rb:

require 'minitest/autorun'

class TestInstruments < MiniTest::Unit::TestCase

  def test_guitars_are_guitars
    guitar = Instrument.new
    another_guitar = Instrument.new
    assert_equal guitar, another_guitar, "guitars should be guitars"
  end

end

Pretty easy right? We require our minitest library and create a new class that inherits from MiniTest::Unit::TestCase - because that's how you use minitest :). Then we set up our first little test, create two guitars and try to test the assertion that they are equal. Running this, of course, fails miserably:

Started
E
Finished in 0.000000 seconds.

  1) Error:
test_guitars_are_guitars(TestInstruments):
NameError: uninitialized constant TestInstruments::Instrument
    minstrel.rb:10:in `test_guitars_are_guitars'

1 tests, 0 assertions, 0 failures, 1 errors, 0 skips

And we can meander down a small intro to Test Driven Development if we fancy, but that's not really what I'm trying to show here. Instead, I'll just add our Instrument class that we played with in irb up above. A bit messy because I'm putting all my code in minstrel.rb....

require 'minitest/autorun'

class Instrument

  def play
    puts 'lovely note'
  end

end

class TestInstruments < MiniTest::Unit::TestCase

  def test_guitars_are_guitars
    guitar = Instrument.new
    another_guitar = Instrument.new
    assert_equal guitar, another_guitar, "guitars should be guitars"
  end

end

Now I can run the whole shebang again:

  1) Failure:
test_guitars_are_guitars(TestInstruments) [minstrel.rb:12]:
No visible difference.
You should look at your implementation of Instrument#==.
#

1 tests, 1 assertions, 1 failures, 0 errors, 0 skips

That' very interesting - look - it fails the assertion, but it actually tells us that there is no visible difference. Further, it points us nicely towards the method we should be examining which is, coincidentally, our equality on Instrument. Of course it is the equality method that is inherited from our base Object class, because as yet, we haven't defined an equality method explicitly.

Defining equality

At last we come to our point - how to override equality on our objects. Baby steps - I'm going to make my assertion pass by overriding equality in my Instrument class and always return true - not our final implementation, but, it is a start:

class Instrument
  
  def ==(other)
    true
  end

  def play
    puts 'lovely note'
  end

end

Running my tests now gives this output

# Running tests:

.

Finished tests in 0.000542s, 1845.7745 tests/s, 1845.7745 assertions/s.

1 tests, 1 assertions, 0 failures, 0 errors, 0 skips

Great success

We have successfully overridden our equality method. Of course our implementation is not complete - because any instrument will be considered equal to any other instrument at this point. One implementation is to expose an Instrument Type property on Instrument. Initialize any instance with a type and then test the types are the same for our equality. You might end up with something like this:

minstrel.rb:

require 'minitest/autorun'

class Instrument
  attr_reader :type

  def initialize(type)
    @type = type
  end

  def ==(other)
    @type == other.type
  end

  def play
    puts 'lovely note'
  end
end

class TestInstruments < MiniTest::Unit::TestCase

  def test_guitars_are_guitars
    guitar = Instrument.new('guitar')
    another_guitar = Instrument.new('guitar')
    assert_equal guitar, another_guitar, "guitars should be equal"
  end

  def test_guitars_are_not_violings
    guitar = Instrument.new('guitar')
    violin = Instrument.new('violin')
    refute_equal guitar, violin, "guitars should not be violins"
  end

end

All over overriding

That's it - we started out on a simple mission to override our method to test equality between classes, came up with a contrived example that differs from the standard Currency example in Kent Beck's book, and ended up with a little test suite toboot. The take home message - override equality by defining your own method called "==" with one param of (other).

No comments:

Post a Comment