RSpec: testing controllers outside of a Rails application

While testing my new gem, restful-controller, I needed to test a controller with RSpec outside of a Rails application.

It proved to be a little bit tricky and I'd like to share my experience.

Gems

We'll need actionpack and activesupport to emulate a Rails application. We'll need rspec-rails for actually testing Rails controllers.

gem.add_development_dependency 'rspec-rails'
gem.add_development_dependency 'actionpack'
gem.add_development_dependency 'activesupport'

Fixtures

Create a spec/fixtures directory with the following files:

fixtures/application.rb

This file will contain a minimum viable Rails application. Props go to the authors of thedecent_exposure gem where I found this technique.

require 'active_support/all'
require 'action_controller'
require 'action_dispatch'

module Rails
  class App
    def env_config; {} end
    def routes
      return @routes if defined?(@routes)
      @routes = ActionDispatch::Routing::RouteSet.new
      @routes.draw do
        resources :posts # Replace with your own needs
      end
      @routes
    end
  end

  def self.application
    @app ||= App.new
  end
end

fixtures/controllers.rb

Define a TestController which all other controllers will inherit from.

class TestController < ActionController::Base
  include Rails.application.routes.url_helpers

  def render(*attributes); end
end

class PostsController < TestController
  restful_controller # This is the gem I'm testing, it will create all 7 default CRUD actions for me.
end

A couple of things going on:

Firstly, we need to include url_helpers manually for redirects to work.

Secondly, if you don't have access to a controller's actions (in my case they are created by the gem, so I can't change them), you'll need to redefine the render method, otherwise you'll get an ActionView::MissingTemplate error.

This is due to the way RSpec and Rails work: even though RSpec won't render templates in controller tests, it still requires the file to be physically present. If you do have access to the actions, you can simply use render nothing: true.

Another way of solving this issue is to create an empty view file for every controller action out there, put it inside something like fixtures/views/posts/index.html.erb and then add the views path to the application config:

config.paths['app/views'] << "spec/fixtures/views" if Rails.env.test?

Needless to say, I prefer to simply redefine the render method.

Tests

Create spec/controller_test.rb:

require 'fixtures/application'
require 'fixtures/controllers'
require 'rspec/rails'

describe PostsController, type: :controller do
  describe '#index' do
    get :index
    # ...
  end
end

We need to use type: :controller because our file is not in the spec/controllers directory, so RSpec doesn't know it's a controller test. If you put the spec inside the controllers directory, you can omit it.

That's it, you can now test controllers without needing to create a full-blown dummy Rails application. The tests are very fast, too.

You're the 15190th person to read this article.