I discovered, when preparing for a different post, that defining interfaces can be generalised.

One can subclass Module (wat?) and provide a hash of signatures that define an interface…

class Interface < Module
  def initialize(signatures)
    @signatures = signatures
  end

  def included(other)
    super
    define_interface
  end

  private

  def define_interface
    @signatures.each do |name, signature|
      test_ivar = "correctly_invoked_#{name.to_s.sub(/\?|\!\z/, '')}"
      class_eval <<-METHODS
  def #{name}(#{signature})
    # define in class, this is for testing
    @#{test_ivar} = true
  end
  def has_#{test_ivar}?() !!@#{test_ivar}; end
  METHODS
    end
  end
end

This means that one can define these Interface modules like this…

module Interfaces
  PermittableInterface = Interface.new(permits?: '')
  ApplicableInterface = Interface.new(applicable?: '*args, &block')
  ServiceInterface = Interface.new(run: '*args')
  ActionInterface = Interface.new(create: '', failure: 'model', success: 'model', not_permitted: '')
end

Note: in the previous post Unit testing, POODR and collaborator interactions I manually defined these Interfaces with a private method that was to be redefined in the included class but this is not necessary.

Then, in your test suite, you can define something like this…

class BasePolicy
  include Interfaces::ApplicableInterface
  def applicable?() super; test_outcome; end
  private
  def test_outcome() fail "No Impl"; end
end

module NotApplicable
  private
  def test_outcome; false; end
end

module IsApplicable
  private
  def test_outcome; true; end
end

class DiscountablePolicy < BasePolicy
  include IsApplicable
end

class NonDiscountablePolicy < BasePolicy
  include NotApplicable
end

So rather than specifying mocks for dependencies, you can use real substitute objects that (should) have the same interface as the production object

However, in the previous post I said that the spec would be immune to the details of the interface definition but this was naive, instead we get readible matchers…

  expect(service).to have_correctly_invoked_run
  expect(discount_rule).to have_correctly_invoked_applicable

What if Reek or Rubocop could be extended to warn for cases where an Interface has been included but not fully implemented?



blog comments powered by Disqus

Published

21 April 2014

Category

ruby

Tags