Skip to content
Nicolas Viennot edited this page Feb 19, 2013 · 4 revisions

Promiscuous provides tools to facilitate TDD and BDD. The design of the Promiscuous test framework allows applications to be tested independently of each other, while providing strong guarantees.

Exporting Publishers Definitions

To be able to test subscribers, we must have knowledge of the publisher definitions.

To do so, Promiscuous provides a command line tool:

bundle exec promiscuous mocks -o generated_mocks.rb

To generate the mock file programmatically, you may use:

File.open('generated_mocks.rb') do |f|
  f.write Promiscuous::Publisher::MockGenerator.generate
end

This is an example of the generated mocks file:

module Crowdtap::Publishers
  class User
    include Promiscuous::Publisher::Model::Mock
    publish :to => 'crowdtap/user'
    mock    :id => :bson

    publish :name
    publish :email
    publish :group_id
  end
  class Member < User
    publish :state
  end
  class Admin < User
  end

  class UserGroup
    include Promiscuous::Publisher::Model::Mock
    publish :to => 'crowdtap/user_group'
    mock    :id => :bson

    publish :name
  end
end

Unit Testing

Promiscuous includes a checkers to validates definitions.

Publisher side

On the Publisher side, execute the following code in your test suite. It generates exceptions with comprehensive error messages to guide the developer.

Promiscuous::Publisher.validate('path/to/generated_mocks.rb')

The following rules are checked:

  • Your mock file is up to date
    Promiscuous will check that the mock file corresponds to what is really published in the application.
  • All the published attributes getter methods must exist
    Promiscuous do so by instantiating all publishers, including subclasses, to verify that instances respond to all published attributes.

Subscriber side

On the subscriber side, the exported mocks must be required before running the validator. Once done, you may use the following command to validate the subscribed models:

Promiscuous::Subscriber.validate

The following rules are checked:

  • All the subscribed attributes getter methods must exist
    Promiscuous performs the check similarly to the publisher side.
  • All the subscribed classes must be published
    Promiscuous checks that the corresponding endpoint exists.
  • All the subscribed subclasses must be published
    Promiscuous checks that all subscribed subclasses map to existing published subclasses.
  • All the subscribed attributes must be published
    Promiscuous checks that all the subscribed attributes are published.

Integration Testing

Promiscuous API definitions differ from Thrift or Protocol Buffers as it is not strongly typed. We believe type checking is too weak for the level of robustness we want to achieve. Rather, Promiscuous combines mocks and factories to allow integration testing on the subscriber side.

Notice that the mocks file (from the example above) are actual classes that behave like models. Once loaded, to simulate operations on a given user, one would do for example:

user = Crowdtap::Publishers::Member.create(:name => 'John')
user.update_attributes(:points => 123)

Promiscuous generates the appropriate JSON payload corresponding to each operation and sends it to the subscriber pipeline. The operations are processed synchronously.

The mocks are best used with factories. The following shows an example with Factory Girl, but you can use Fabrication, Machinist, or regular fixtures, you name it.

Both mocks and the factories are provided by the publisher app. Promiscuous uses factories to describe the semantics of the data that will be published.

Publisher side

Pair the mock file example with this published factory file (FactoryGirl style):

module Crowdtap::Publishers
  FactoryGirl.define do
    sequence :crowdtap_email { |n| "user#{n}@example.com" }

    factory :crowdtap_user, :class => User do
      name  "John"
      email { FactoryGirl.generate :crowdtap_email }
      association :group, :factory => :crowdtap_group

      factory :crowdtap_admin, :class => Admin

      factory :crowdtap_member, :class => Member do
        state 'active'
      end

      Member.class_eval do
        def ban!
          update_attributes(:state => 'banned')
        end
      end
    end

    factory :crowdtap_user_group, :class => UserGroup do
      name "Some user group"
    end
  end
end

Subscriber side

class Member
  include Mongoid::Document
  include Promiscuous::Subscriber

  subscribe do
    field :name
    field :email
  end
end

class Member
  subscribe do
    field :state
  end

  def got_banned?
    state_changed? && state == 'banned'
  end

  after_create do
    Mailer.send_email(:member_id => self.id, :type => :banned) if got_banned?
  end
end

The integration test for the subscriber becomes (RSpec style):

describe Member do
  subject { create(:crowdtap_member) }

  context 'when the user gets banned' do
    before { subject.ban! }
    it 'receives an ban email' do
      Mailer.sent_emails.first.to.should   == subject.email
      Mailer.sent_emails.first.body.should =~ /#{subject.name}/
      Mailer.sent_emails.first.body.should =~ /You got banned/
    end
  end
end

A best practice when writing integration tests is to provide helper methods to do state transitions on published models, like ban! for two reasons:

  1. The publisher is the owner of the Member model. It is the one responsible for the semantics of the data changes that a subscriber may observe.
  2. When you start having many subscriber to the same publisher, you don't repeat yourself when it comes to the behavior of published data.

Gemify your Apps

We found that using gems is a great way to efficiently export the mocks and factories files to the subscriber applications. Example:

Publisher side

# file: ./api/crowdtap/publishers.rb
# Generated mock file (omitted)

# file: ./api/crowdtap/factories.rb
# Factories (omitted)

# file: ./api/crowdtap.rb
require 'active_support'
module Crowdtap
  autoload :Publishers, 'crowdtap/publishers'
end

# file: ./crowdtap.gemspec
Gem::Specification.new do |s|
  s.name    = "crowdtap"
  s.version = "1.0"
  s.summary = "Crowdtap API"
  s.files   = Dir["api/**/*"]
  s.require_path = 'api'
end

Subscriber side

# file: ./Gemfile
gem 'crowdtap', :git => 'git@github.com:crowdtap/crowdtap.git'
# for local development:
gem 'crowdtap', :path => '~/crowdtap'

# file: ./spec/factories.rb
load 'crowdtap/factories.rb'

FactoryGirl.define do
  # local factories
end

At Crowdtap, we also use these gems to make internal synchronous APIs available to other applications. The benefit is twofold:

  1. It makes the integration testing much easier (no tests on the actual wire prototocol).
  2. The owner of the API can change the underlying protocol without having to change the users of the API.