Skip to content

Instantly share code, notes, and snippets.

@ches
Last active November 29, 2018 01:34
Show Gist options
  • Star 11 You must be signed in to star a gist
  • Fork 3 You must be signed in to fork a gist
  • Save ches/4993289 to your computer and use it in GitHub Desktop.
Save ches/4993289 to your computer and use it in GitHub Desktop.
Example of testing Rails observers in isolation for cross-cutting concerns
require 'spec_helper'
# Bustle is a pubsub system used for activity streams:
# https://github.com/fredwu/bustle
#
# Here when a person follows another (or a discussion, for instance), the observer wires
# up pubsub between them for future activity notifications. The Follow model knows nothing
# about the implementation choices for the pubsub system.
describe FollowObserver do
subject { FollowObserver.instance }
let(:follow) { Follow.make!(:user) }
let(:follower) { follow.user }
let(:followable) { follow.followable }
describe 'after_create' do
it 'subscribes follower to future activity from the followable' do
expect { subject.after_create(follow) }.
to subscribe(follower).to(followable)
end
it 'publishes followed activity for the followable' do
expect { subject.after_create(follow) }.
to publish_activity('followed').about(followable).by(follower)
end
it 'publishes followed by activity for the followable' do
expect { subject.after_create(follow) }.
to publish_activity('been_followed_by').about(follower).by(followable)
end
end
describe 'before_destroy' do
before(:each) do
Follow.observers.enable(:all) { follow }
end
it 'unsubscribes follower from future activity from the followable' do
publisher = Bustle::Publishers.get(followable)
subscriber = Bustle::Subscribers.get(follower)
expect { subject.before_destroy(follow) }.
to change { Bustle::Subscriptions.get(publisher, subscriber) }
end
end
end
# I wish I had a better example at the moment without the distraction of the extra
# code for these custom matchers, but realize that they serve to make assertions
# succinct -- they have no bearing on on the setup. If concerns are not overlapping,
# such isolated observer specs shouldn't have a lot of pain with setting up model
# instance state.
RSpec::Matchers.define :publish_activity do |action|
chain :about do |resource|
@resource = resource
end
chain :by do |publisher_resource|
@publisher = Bustle::Publishers.get(publisher_resource)
end
match do |expect_proc|
expect_proc.call
conditions = { action: action }
conditions.merge!(publisher_id: @publisher.id) if @publisher
if @resource
conditions.merge!(
resource_id: @resource.id,
resource_class: @resource.class.name
)
end
Bustle::Activity.where(conditions).one?
end
end
RSpec::Matchers.define :subscribe do |subscriber_resource|
chain :to do |publisher_resource|
@publisher = Bustle::Publishers.add publisher_resource
end
match do |expect_proc|
expect_proc.call
subscriber = Bustle::Subscribers.add subscriber_resource
conditions = { subscriber_id: subscriber.id }
conditions.merge!(publisher_id: @publisher.id) if @publisher
Bustle::Subscription.where(conditions).one?
end
end
RSpec.configure do |config|
# Assure we're testing models in isolation from Observer behavior. Enable
# them explicitly in a block if you need to integration test an Observer --
# see the documentation for {ActiveModel::ObserverArray}.
config.before do
ActiveRecord::Base.observers.disable :all
end
# Integration tests are full-stack, lack of isolation is by design.
config.before(type: :feature) do
ActiveRecord::Base.observers.enable :all
end
end
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment