Skip to main content

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).

Comments

Popular posts from this blog

Getting started with Ruby on Rails 3.2 and MiniTest - a Tutorial

For fun, I thought I would start a new Ruby on Rails project and use MiniTest instead of Test::Unit. Why? Well MiniTest is Ruby 1.9s testing framework dejour, and I suspect we will see more and more new projects adopt it. It has a built in mocking framework and RSpec like contextual syntax. You can probably get away with fewer gems in your Gemfile because of that. Getting started is always the hardest part - let's jump in with a new rails project rails new tddforme --skip-test-unit Standard stuff. MiniTest sits nicely next to Test::Unit, so you can leave it in if you prefer. I've left it out just to keep things neat and tidy for now. Now we update the old Gemfile: group :development, :test do gem "minitest" end and of course, bundle it all up.....from the command line: $ bundle Note that if you start experiencing strange errors when we get in to the generators later on, make sure you read about rails not finding a JavaScript runtime . Fire up

Getting started with Docker

Docker, in the beginning, can be overwhelming. Tutorials often focus on creating a complex interaction between Dockerfiles, docker-compose, entrypoint scripts and networking. It can take hours to bring up a simple Rails application in Docker and I found that put me off the first few times I tried to play with it. I think a rapid feedback loop is essential for playing with a piece of technology. If you've never used Docker before, then this is the perfect post for you. I'll start you off on your docker journey and with a few simple commands, you'll be in a Docker container, running ruby interactively. You'll need to install Docker. On a Mac, I prefer to install Docker Desktop through homebrew: brew cask install docker If you're running Linux or Windows, read the official docs for install instructions. On your Mac, you should now have a Docker icon in your menu bar. Click on it and make sure it says "Docker desktop is running". Now open a terminal and ty

Rails 3.2, MiniTest Spec and Capybara

What do you do when you love your spec testing with Capybara but you want to veer off the beaten path of Rspec and forge ahead into MiniTest waters? Follow along, and you'll have not one, but two working solutions. The setup Quickly now, let's throw together an app to test this out. I'm on rails 3.2.9. $ rails new minicap Edit the Gemfile to include a test and development block group :development, :test do gem 'capybara' gem 'database_cleaner' end Note the inclusion of database_cleaner as per the capybara documentation And bundle: $ bundle We will, of course, need something to test against, so for the sake of it, lets throw together a scaffold, migrate our database and prepare our test database all in one big lump. If you are unclear on any of this, go read the guides . $ rails g scaffold Book name:string author:string $ rake db:migrate $ rake db:test:prepare Make it minitest To make rails use minitest , we simply add a require