← Back to Upcase

Rspec tests for an Abstract Interface. (With separate implementation gems.)

(Brian Cameron) #1

Hi folks,
This is my first post. Thanks for all the fantastic content. I’ve been learning a lot.

Context:

I’ve being implementing an abstract calendar interface which will require several implementations. The abstract interface represents the operations that I would like in one gem, each implementation is in its own isolated gem. (For example: gem ‘calendar’ and gem ‘calendar-outlook’.)

I have included a sketch of the code below. I have based myself on the structure observed in the ‘omniauth’ and ‘omniauth-facebook’ gems for example, which uses a module to act as a container for provided implementations. (With this approach one can then use Calendar::Implementations.constants and Calendar::Implementations.const_get to build out a Calendar::Factory.)

Question:

This is all working very well for me. So here is my real question:

How can I write a set of rspec tests once in the ‘calendar’ gem and have them run against the ‘calendar-outlook’ (instance of) while I’m working on the implementation gem.

Basically, I need to run two sets of tests:
(1) The ‘internal’ specs from the ‘calendar-outlook’ gem.
(2) Create an instance of Calendar::Implementations::Outlook in the ‘calendar-outlook’ gem and test it against the api specs provided in the ‘calendar’ gem.

Has anybody got any ideas as how to approach this?

Current Approach:

My current approach is very ugly: The ‘calendar’ gem instantiates all implementations (each implementation provides a mock credential) and runs the tests against each implementation (I even do an each block in every test… This is horrible!) I don’t want to be running the specs of the abstract gem when I’m working on the implementation and it also requires the abstract gem to know about the implementations which is far from ideal. This creates a kind of circular reference which I really, really don’t feel comfortable with. (I’m surprised this worked actually.) I’m sure there are many more issues with this approach but I guess it’s pretty clear that this is not clean.

There you go, I’d be very happy to hear if anybody has some suggestions! :smile:
Thanks in advance,
Brian.

require 'singleton'

# gem 'calendar'
module Calendar
  class Credentials 
    def initialize(interface:, url:, token:, name:, password:)
      ...
    end
  end

  class Base
    def initialize(credentials)
      @credentials = credentials
    end
    attr_accessor :credentials
  end

  module Implementations
    # NOTE: Add your implementation to this module and it will be picked up automatically
    # class [Implementation_Name] < Calendar::Base
    #   def initialize(credentials) ... super ... end
    #   def make_reservation ... end
    #   ...
    # end
  end
  
  class Factory
    include Singleton
    def implementations
      Implementations.constants
    end
    def fetch(credentials)
      implementation = Implementations.const_get(credentials.interface.to_sym)
      implementation.new(credentials)
    end
  end
end

# gem 'calendar-outlook'
module Calendar
  module Implementations
    class Outlook < Calendar::Base
      def initialize(credentials)
        super
        @connection = self.object_id
      end
      def make_reservation
        # Put a real implementation in here
      end
    end
  end
end

# gem 'calendar-google'
module Calendar
  module Implementations
    class Google_Calendar < Calendar::Base
      # Another Implementation
    end
  end
end

credentials = Calendar::Credentials.new(
  interface: interface,
  url: "http://server.com", 
  token: "", 
  name: "fatherted", 
  password: "1234"
)

calendar ||= Calendar::Factory.instance.fetch(calendar_credentials)
calendar.make_reservation(...)