-
Notifications
You must be signed in to change notification settings - Fork 34
Testing
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.
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
Promiscuous includes a checkers to validates definitions.
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.
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.
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.
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
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:
- 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.
- When you start having many subscriber to the same publisher, you don't repeat yourself when it comes to the behavior of published data.
We found that using gems is a great way to efficiently export the mocks and factories files to the subscriber applications. Example:
# 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
# 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:
- It makes the integration testing much easier (no tests on the actual wire prototocol).
- The owner of the API can change the underlying protocol without having to change the users of the API.