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

  1. 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.

  2. Nice post. :)

    Can you post this on the wiki so more people can use it.

  3. @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.

  4. @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).

  5. Carl Woodward Carl Woodward

    Oct 29, 2008

    This looks great. Just a couple of things. I think it could be dry’ed up a bit. The:

    
    controller.stub!(:ensure_authenticated)
    controller.stub!(:ensure_admin)
    controller.stub!(:render)
    
    

    And

    
    @user = mock(:users, :destroy =&gt; true)
    User.stub!(:get).and_return(@user)
    
    

    Could be moved out into functions to tighten up the code.

  6. 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.

About

Authored by John Nunemaker (Noo-neh-maker), a programmer who has fallen deeply in love with Ruby. Learn More.

Projects

Flipper
Release your software more often with fewer problems.
Flip your features.