Skip to content

Instantly share code, notes, and snippets.

@patmaddox
Created March 16, 2010 23:58
Show Gist options
  • Save patmaddox/334690 to your computer and use it in GitHub Desktop.
Save patmaddox/334690 to your computer and use it in GitHub Desktop.
# Functional test just to demonstrate that everything is hooked up
describe UsersController, "POST create", :type => :controller do
before(:each) do
UsersController.dispatcher = DefaultDispatcher
end
context "successful" do
it "should create a user" do
lambda {
post :create, :user => {:name => "Pat", :email => "[email protected]"}
}.should change { User.count }.by(1)
end
end
context "failure" do
it "should not create a user" do
lambda {
post :create, :user => {}
}.should_not change { User.count }
end
end
end
# Hey check it out, a really simple sexy controller unit test!
describe UsersController, "POST create" do
before(:each) do
@command = mock('CreateUser command', :success? => true)
Commands::CreateUser.stub(:new).and_return @command
@handler = mock('handler', :dispatch => true)
UsersController.dispatcher = @handler
end
it "should dispatch a CreateUser command" do
@handler.should_receive(:dispatch).with(@command)
post :create, :user => {}
end
it "should pass the params when building the CreateUser command" do
Commands::CreateUser.should_receive(:new).with('param1' => 'foo')
post :create, :user => {:param1 => 'foo'}
end
it "should HTTP 200 when the command is successful" do
@command.stub(:success?).and_return true
post :create, :user => {}
response.should be_success
end
it "should HTTP 400 when the command is unsuccessful" do
@command.stub(:success?).and_return false
post :create, :user => {}
response.code.should == '400'
end
end
# A controller now acts as an adapter to the app's commands
class UsersController < ApplicationController
class << self
attr_accessor :dispatcher
end
def create
command = Commands::CreateUser.new params['user']
dispatch command
if command.success?
render :text => "yay!", :status => 200
else
render :text => "nay :(", :status => 400
end
end
def dispatch(command)
self.class.dispatcher.dispatch command
end
end
describe Commands::CreateUser do
it "should be valid with a name and email" do
Commands::CreateUser.new(:email => '[email protected]', :name => 'Pat').should be_valid
end
it "should be invalid with a missing name" do
Commands::CreateUser.new(:email => '[email protected]', :name => '').should_not be_valid
end
it "should be invalid with a missing email" do
Commands::CreateUser.new(:email => '', :name => 'Pat').should_not be_valid
end
end
# Doesn't follow the Command pattern to the t (no execute method). Commands only know
# about validity. The actual execution is done by command handlers
module Commands
class CreateUser
attr_writer :success
attr_reader :name, :email
def initialize(options)
@name = options[:name]
@email = options[:email]
end
def valid?
[@name, @email].all?(&:present?)
end
def success?
@success
end
end
end
describe EventHandlers::CreateUserRecord do
it "should create a user with the name and email given by the command" do
command = stub('command', :name => 'Pat', :email => '[email protected]')
User.should_receive(:create!).with(:name => 'Pat', :email => '[email protected]')
EventHandlers::CreateUserRecord.new.handle command
end
end
module EventHandlers
class CreateUserRecord
def handle(command)
User.create! :name => command.name, :email => command.email
end
end
end
describe CommandDispatcher do
class FunkyCommand
attr_writer :success
def initialize(valid=true)
@valid = valid
end
def valid?
@valid
end
def success?
@success
end
end
it "should allow handlers to be registered as anonymous blocks" do
dispatcher = CommandDispatcher.new
called = false
dispatcher.register(FunkyCommand) {|command| called = true }
dispatcher.dispatch FunkyCommand.new
called.should be_true
end
it "should only call the handlers registered for a dispatched command type" do
dispatcher = CommandDispatcher.new
called = false
dispatcher.register(FunkyCommand) {|command| called = true }
dispatcher.dispatch Object.new
called.should be_false
end
it "should not call the handler if the command is invalid" do
dispatcher = CommandDispatcher.new
called = false
dispatcher.register(FunkyCommand) {|command| called = true }
dispatcher.dispatch FunkyCommand.new(false)
called.should be_false
end
it "should allow a handler to be registered as a class" do
dispatcher = CommandDispatcher.new
called = false
handler_class = Class.new do
define_method(:handle) {|command| called = true }
end
dispatcher.register FunkyCommand, handler_class
dispatcher.dispatch FunkyCommand.new
called.should be_true
end
it "sets the command's success flag to true if the handler passes" do
dispatcher = CommandDispatcher.new
dispatcher.register(FunkyCommand) {|command| }
command = FunkyCommand.new
dispatcher.dispatch command
command.should be_success
end
it "sets the command's success flag to false if the handler errors" do
dispatcher = CommandDispatcher.new
dispatcher.register(FunkyCommand) {|command| raise "chickens" }
command = FunkyCommand.new
dispatcher.dispatch command
command.should_not be_success
end
end
class CommandDispatcher
def register(command_class, handler_class=nil, &block)
@command_class = command_class
@block = handler_class ? lambda {|command| handler_class.new.handle(command) } : block
end
def dispatch(command)
if @command_class === command && command.valid?
begin
@block.call(command)
command.success = true
rescue
command.success = false
end
end
end
end
class UserRegistrationApp
def initialize(dispatcher)
dispatcher.register Commands::CreateUser, EventHandlers::CreateUserRecord
end
end
# and then in environments/config.rb...
DefaultDispatcher = CommandDispatcher.new
UserRegistrationApp.new DefaultDispatcher
=begin
So here's what I like about this setup...first, it's wicked decoupled. The application's
interface is defined in terms of commands that the client can perform. The application's
behavior is defined by linking up commands to command handlers. The app can be run entirely
headless. In order to use a different UI, you just write a new adapter that converts client
interactions into commands, then dispatch the command.
Whole thing is very proof of concept, obviously. At this point it really only implements
the left side of the hexagonal architecture.
=end
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment