An object quacks like a duck

I've been toying around with the idea of spec'ing mixins: that a class includes a module. Suppose the following class:

class FooList
  include Enumerable
 
  attr_accessor :some_array
  def initialize(opts)
    @some_array = opts[:the_array] || []
  end
 
  def each
    @some_array.each { |item| yield item }
  end
end

We can test the behavior of the each method using RSpec, but we can also make sure that FooList actually acts like an Enumerable. Here's a quick RSpec Matcher just for that (require it in your spec_helper.rb)

Spec::Matchers.define :quack_like do |mod|
  match do |instance|
    mod.instance_methods.inject(true) { |accum, method| accum && instance.respond_to?(method) }
  end

  failure_message_for_should do |instance|
    "expected the class #{instance.class.name} to include the module #{mod}"
  end

  failure_message_for_should_not do |instance|
    "expected the class #{instance.class.name} not to include the module #{mod}"
  end

  description do
    "expected the class to behave like a module by responding to all of its instance methods"
  end
end

This allows us to spec some quacking:

describe FooList do
  def foo_list
    @foo_list ||= FooList.new
  end

  it "quacks like an Enumerable" do
    foo_list.should quack_like Enumerable
  end
end

I am still experimenting with this. In a way it is not really testing behavior, but it's not really testing the implementation either. In other words, if every method in Enumerable is implemented in FooList and we remove the include Enumerable line, the spec still passes.

I've discussed this over IRC with some other smart folks, but I want more input . Do you think this is appropriate? Useless?

Tagged as: bdd, ruby, rspec
Fork me on GitHub