October 28, 2008
Older: Using Gmail with IMAP to Receive Email in Rails
Newer: Can't Miss Coverage of RubyConf Day 1
Testing Merb Controllers with RSpec
Update: Atmos has written a far better tutorial than me on this and shows the “proper” way to test. Please check that out instead of reading my post. :)
I think web hooks are sweet. The idea of making micro apps that take the pain out of typically painful things and allow for ridiculous re-use is really intriguing to me. In my receiving email research, I came across a few web hook apps that work with email (ie: MailHook). Who wants to keep messing with postfix for every app they want to receive email with? I don’t, that is for sure. That is why I got pretty stoked when I saw technoweenie start working on an email web hook app astutely named AstroTrain. I almost immediately forked it and started playing around.
Astrotrain is written with Merb and DataMapper. Merb has changed a lot since I last played with it so it took me a while to get my bearings. Now that I have, I’m feeling more comfortable, yet still occasionally frustrated. Because the API has changed so much, it’s kind of hard to find up to date examples of how to get things done in Merb. I’m not going to go in depth on why I did what I did, but I thought I would show the simple users controller I added to Astrotrain and the specs to make sure it is behaving. If there are any Merbists out there who have suggestions for making this more merb-ish, let me know in the comments and I’ll update accordingly.
app/controllers/users.rb
class Users < Application
before :ensure_authenticated
before :ensure_admin
def index
@users = User.all
render
end
def show(id)
@user = User.get(id)
raise NotFound unless @user
@mappings = @user.mappings
render
end
def new
@user = User.new
render
end
def create(user)
@user = User.new(user)
if @user.save
redirect url(:users), :message => {:notice => "User was successfully created"}
else
render :new
end
end
def edit(id)
@user = User.get(id)
raise NotFound unless @user
render
end
def update(user)
@user = User.get(params[:id])
raise NotFound unless @user
if @user.update_attributes(user)
redirect url(:users), :message => {:notice => "User was successfully updated"}
else
render :show
end
end
def destroy(id)
@user = User.get(id)
raise NotFound unless @user
if @user.destroy
redirect url(:users)
else
raise InternalServerError
end
end
end
spec/controllers/users_spec.rb
require File.join(File.dirname(__FILE__), '..', 'spec_helper.rb')
describe "UserNotFound", :shared => true do
before do
User.stub!(:get).and_return(nil)
end
it "should raise NotFound" do
lambda {
do_request
}.should raise_error
end
end
describe Users do
describe "GET index" do
def do_request
dispatch_to(Users, :index) do |controller|
controller.stub!(:ensure_authenticated)
controller.stub!(:ensure_admin)
controller.stub!(:render)
end
end
it "should be successful" do
do_request.should be_successful
end
it "should assign users for the view" do
users = [mock(:user), mock(:user)]
User.should_receive(:all).and_return(users)
do_request.assigns(:users).should == users
end
end
describe "GET show" do
before do
@user = mock(:user)
@mappings = [mock(:mapping)]
@user.stub!(:mappings).and_return(@mappings)
User.stub!(:get).and_return(@user)
end
def do_request
dispatch_to(Users, :show, :id => 1) do |controller|
controller.stub!(:ensure_authenticated)
controller.stub!(:ensure_admin)
controller.stub!(:render)
end
end
it "should be successful" do
do_request.should be_successful
end
it "should assign user for the view" do
User.should_receive(:get).with('1').and_return(@user)
do_request.assigns(:user).should == @user
end
end
describe "GET show (with missing user)" do
def do_request
dispatch_to(Users, :show, :id => 1) do |controller|
controller.stub!(:ensure_authenticated)
controller.stub!(:ensure_admin)
controller.stub!(:render)
end
end
it_should_behave_like 'UserNotFound'
end
describe "Get new" do
before do
@user = mock(:user)
User.stub!(:new).and_return(@user)
end
def do_request
dispatch_to(Users, :new) do |controller|
controller.stub!(:ensure_authenticated)
controller.stub!(:ensure_admin)
controller.stub!(:render)
end
end
it "should assign user for view" do
User.should_receive(:new).and_return(@user)
do_request.assigns(:user).should == @user
end
it "should be successful" do
do_request.should be_successful
end
end
describe "POST create (with valid user)" do
before do
@attrs = {'login' => 'jnunemaker'}
@user = mock(:user, :save => true)
User.stub!(:new).and_return(@user)
end
def do_request
dispatch_to(Users, :create, :user => @attrs) do |controller|
controller.stub!(:ensure_authenticated)
controller.stub!(:ensure_admin)
controller.stub!(:render)
end
end
it "should assign new user" do
User.should_receive(:new).with(@attrs).and_return(@user)
do_request.assigns(:user).should == @user
end
it "should save the user" do
@user.should_receive(:save).and_return(true)
do_request
end
it "should redirect" do
do_request.should redirect_to(url(:users))
end
end
describe "POST create (with invalid user)" do
before do
@attrs = {'login' => ''}
@user = mock(:user, :save => false)
User.stub!(:new).and_return(@user)
end
def do_request
dispatch_to(Users, :create, :user => @attrs) do |controller|
controller.stub!(:ensure_authenticated)
controller.stub!(:ensure_admin)
controller.stub!(:render)
end
end
it "should assign new user" do
User.should_receive(:new).with(@attrs).and_return(@user)
do_request.assigns(:user).should == @user
end
it "should attempt to save the user" do
@user.should_receive(:save).and_return(false)
do_request
end
it "should be successful" do
do_request.should be_successful
end
end
describe "GET edit" do
before do
@user = mock(:user)
@mappings = [mock(:mapping)]
@user.stub!(:mappings).and_return(@mappings)
User.stub!(:get).and_return(@user)
end
def do_request
dispatch_to(Users, :edit, :id => 1) do |controller|
controller.stub!(:ensure_authenticated)
controller.stub!(:ensure_admin)
controller.stub!(:render)
end
end
it "should be successful" do
do_request.should be_successful
end
it "should assign user for the view" do
User.should_receive(:get).with('1').and_return(@user)
do_request.assigns(:user).should == @user
end
end
describe "GET edit (with missing user)" do
def do_request
dispatch_to(Users, :edit, :id => 1) do |controller|
controller.stub!(:ensure_authenticated)
controller.stub!(:ensure_admin)
controller.stub!(:render)
end
end
it_should_behave_like 'UserNotFound'
end
describe "PUT update (with valid user)" do
before do
@attrs = {'login' => 'jnunemaker'}
@user = mock(:user, :update_attributes => true)
User.stub!(:get).and_return(@user)
end
def do_request
dispatch_to(Users, :update, :id => 1, :user => @attrs) do |controller|
controller.stub!(:ensure_authenticated)
controller.stub!(:ensure_admin)
controller.stub!(:render)
end
end
it "should assign user" do
User.should_receive(:get).with('1').and_return(@user)
do_request.assigns(:user).should == @user
end
it "should update the user's attributes" do
@user.should_receive(:update_attributes).and_return(true)
do_request
end
it "should redirect" do
do_request.should redirect_to(url(:users))
end
end
describe "PUT update (with invalid user)" do
before do
@attrs = {'login' => ''}
@user = mock(:user, :update_attributes => false)
User.stub!(:get).and_return(@user)
end
def do_request
dispatch_to(Users, :update, :id => 1, :user => @attrs) do |controller|
controller.stub!(:ensure_authenticated)
controller.stub!(:ensure_admin)
controller.stub!(:render)
end
end
it "should assign new user" do
User.should_receive(:get).with('1').and_return(@user)
do_request.assigns(:user).should == @user
end
it "should attempt to update the user's attributes" do
@user.should_receive(:update_attributes).and_return(false)
do_request
end
it "should be successful" do
do_request.should be_successful
end
end
describe "PUT update (with missing user)" do
def do_request
dispatch_to(Users, :update, :id => 1) do |controller|
controller.stub!(:ensure_authenticated)
controller.stub!(:ensure_admin)
controller.stub!(:render)
end
end
it_should_behave_like 'UserNotFound'
end
describe "DELETE destroy" do
before do
@user = mock(:users, :destroy => true)
User.stub!(:get).and_return(@user)
end
def do_request
dispatch_to(Users, :destroy, :id => 1) do |controller|
controller.stub!(:ensure_authenticated)
controller.stub!(:ensure_admin)
controller.stub!(:render)
end
end
it "should find the user" do
User.should_receive(:get).with('1').and_return(@user)
do_request
end
it "should destroy the user" do
@user.should_receive(:destroy).and_return(true)
do_request
end
it "should redirect" do
do_request.should redirect_to(url(:users))
end
end
describe "DELETE destroy (with missing user)" do
def do_request
dispatch_to(Users, :destroy, :id => 1) do |controller|
controller.stub!(:ensure_authenticated)
controller.stub!(:ensure_admin)
controller.stub!(:render)
end
end
it_should_behave_like 'UserNotFound'
end
end
I haven’t really spec’d everything. I should probably add stuff to make sure it doesn’t work if the user is not authenticated or is not an admin. It might even be good to test the views a bit, though they are pretty simple. Hope this helps others starting to dive into the 1.0 merb release candidates.
AstroTrain is only really ready for the brave but rest assured that when it is ready to hit the big time, I’ll post here again with a how to get it running and integrated with your app.
6 Comments
Oct 29, 2008
Thanks for the post. I was just wrestling with testing my Merb controller. I’m checking out the generated specs with merb-gen resource_controller and it creates this spec/requests/users_spec.rb. It tests that the response is successful and the redirections, so I’m not sure if that’s the Merb way. Does that mean that the tests for successful responses and redirections should be extracted from your spec to the requests folder? I’m still trying things out and haven’t decided yet.
Btw, a minor comment on your controller above. Your update action can receive the parameters (id, user), so you can just pass id instead of params[:id], for consistency at least since you’re using it in your other actions. I’m also using the resource method instead of the url method in my redirections.
Oct 29, 2008
Nice post. :)
Can you post this on the wiki so more people can use it.
Oct 29, 2008
@mikong – Ah, nice on the id, user. I’ll give that a try.
@Emil – Someone beat me to it. They must have seen your suggestion.
Oct 29, 2008
@John – It was me :). What I meant is that all of this is copied so all of the information is on a single source (the wiki).
Oct 29, 2008
This looks great. Just a couple of things. I think it could be dry’ed up a bit. The:
And
Could be moved out into functions to tighten up the code.
Nov 01, 2008
RSpec and Merb – sounds nice.
@John – did you change Rails on Merb?
Sorry, comments are closed for this article to ease the burden of pruning spam.