• Stars
    star
    468
  • Rank 93,767 (Top 2 %)
  • Language
    JavaScript
  • License
    MIT License
  • Created almost 13 years ago
  • Updated over 8 years ago

Reviews

There are no reviews yet. Be the first to send feedback to the community and the maintainers!

Repository Details

Focused Controller

Build Status

Focused Controller alters Rails' conventions so that each individual action in a controller is represented by its own class. This makes it easier to break up the code within an action and share code between different actions.

Focused Controller also provides test helpers which enable you to write unit tests for your controller code. This is much faster than functional testing, and better suited to testing fine grained logic that may exist in your actions.

I wrote a blog post to fully explain the idea.

There is a mailing list for discussion.

Synopsis

class ApplicationController
  class Action < ApplicationController
    include FocusedController::Mixin
  end
end

module PostsController
  class Action < ApplicationController::Action
    before_filter :authenticate
  end

  class Index < Action
    expose(:posts) { Post.recent.limit(5) }
  end

  class New < Action
    expose(:post) { Post.new }
  end

  class Singular < Action
    expose(:post) { Post.find params[:id] }
    before_filter { redirect_to root_path unless post.accessible_to?(current_user) }
  end

  class Show < Singular
  end

  class Update < Singular
    def call
      if post.update_attributes(params[:post])
        # ...
      else
        # ...
      end
    end
  end
end

You can include FocusedController::Mixin anywhere, so you don't have to use Focused Controller in every single controller if you don't want to. I find it useful to define ApplicationController::Action and inherit from that where needed.

The #call method is what gets invoked when the request is served.

The #expose declaration defines a method which runs the block and memoizes the result. It also makes post a helper method so you can call it from the view template.

The before_filter in Singular is inherited by precisely the actions that need it, so we don't need to specify :only or :except.

Routing

Rails' normal routing assumes your actions are methods inside an object whose name ends with 'controller'. For example:

get '/posts/new' => 'posts#new'

will route GET /posts/new to PostsController#new.

To get around this, we use the focused_controller_routes helper:

Loco2::Application.routes.draw do
  focused_controller_routes do
    get '/posts/new' => 'posts#new'
  end
end

The route will now map to PostsController::New#call.

All the normal routing macros are also supported:

focused_controller_routes do
  resources :posts
end

Functional Testing

If you wish, focused controllers can be tested in the classical 'functional' style. It no longer makes sense to specify the method name to be called as it would always be #call. So this is omitted:

require 'focused_controller/functional_test_helper'
require_dependency 'users_controller'

module UsersController
  class CreateTest < ActionController::TestCase
    include FocusedController::FunctionalTestHelper

    test "should create user" do
      assert_difference('User.count') do
        post user: { name: 'Jon' }
      end

      assert_redirected_to user_path(@controller.user)
    end
  end
end

There is also an equivalent helper for RSpec:

require 'focused_controller/rspec_functional_helper'

describe UsersController do
  include FocusedController::RSpecFunctionalHelper

  describe UsersController::Create do
    it "should create user" do
      expect { post user: { name: 'Jon' } }.to change(User, :count).by(1)
      response.should redirect_to(user_path(subject.user))
    end
  end
end

Unit Testing

Unit testing is faster and better suited to testing logic than functional testing. To do so, you instantiate your action class and call methods on it:

module UsersController
  class ShowTest < ActiveSupport::TestCase
    test 'finds the user' do
      user = User.create

      controller = UsersController::Show.new
      controller.params = { id: user.id }

      assert_equal user, controller.user
    end
  end
end

The #call method

Testing the code in your #call method is a little more involved, depending on what's in it. For example, your #call method may use (explicitly or implicitly) any of the following objects:

  • request
  • response
  • params
  • session
  • flash
  • cookies

To make the experience smoother, Focused Controller sets up mock versions of these objects, much like with classical functional testing. It also provides accessors for these objects in your test class.

require 'focused_controller/test_helper'
require_dependency 'users_controller'

module UsersController
  class CreateTest < ActiveSupport::TestCase
    include FocusedController::TestHelper

    test "should create user" do
      controller.params = { user: { name: 'Jon' } }

      assert_difference('User.count') do
        controller.call
      end

      assert_redirected_to user_path(controller.user)
    end
  end
end

Assertions

You have access to the normal assertions found in Rails' functional tests:

  • assert_template
  • assert_response
  • assert_redirected_to

Filters

In unit tests, we're not testing through the Rack stack. We're just calling the #call method. Therefore, filters do not get run. If some filter code is crucial to what your action is doing then you should move it out of the filter. If the filter code is separate, then you might want to unit-test it separately, or you might decide that covering it in integration/acceptance tests is sufficient.

RSpec

There is a helper for RSpec as well:

require 'focused_controller/rspec_helper'

describe UsersController do
  include FocusedController::RSpecHelper

  describe UsersController::Create do
    it "creates a user" do
      subject.params = { user: { name: 'Jon' } }
      expect { subject.call }.to change(User, :count).by(1)
      response.should redirect_to(user_path(subject.user))
    end
  end
end

More examples

The acceptance tests for Focused Controller exercise a complete Rails application, which uses the plugin. Therefore, you might wish to look there to get more of an idea about how it can be used.

(Note that the code there is based on Rails' scaffolding, not how I would typically write controllers and tests.)

More Repositories

1

spring-commands-rspec

Ruby
376
star
2

date_input

Date Input is a no frills date picker plugin for jQuery
JavaScript
107
star
3

spring-docker-example

Ruby
56
star
4

gedit-trailsave

Strip trailing whitespace on save
Python
34
star
5

conductor

Conductor is a Rails plugin which helps you manage creating and updating a record and its associations via a single form submission
Ruby
17
star
6

spring-commands-cucumber

Ruby
9
star
7

path_glob

A glob-matching library for Elixir
Elixir
9
star
8

poisson

A Ruby library for using the Poission distribution
Ruby
8
star
9

spring-commands-testunit

Ruby
6
star
10

bcms_feeds

A BrowserCMS module which fetches, caches and displays RSS/Atom feeds
JavaScript
6
star
11

find_or_initialize_resource

Abstracts the finding and initializing of resources within your controller
Ruby
4
star
12

vim-test-vimterminal-enhanced

Vim Script
4
star
13

delegate_temporal

A workaround to ensure multiparameter assignment still works when you delegate date/time Active Record attributes
Ruby
4
star
14

rails_html_output

Make Action View produce HTML rather than XHTML
Ruby
4
star
15

inactive_record

Make a non ActiveRecord object "quack" like an ActiveRecord
Ruby
4
star
16

delegate_with_default

Rails plugin to add a :default option to the delegate method
Ruby
4
star
17

restrict_params

Restrict the params which we allow to get through to the action
Ruby
4
star
18

dependent_records

Lists the associated records which will be destroyed by destroying a particular record
Ruby
4
star
19

restflection

A Rails plugin to reflect information about resources
Ruby
3
star
20

default_templates

Allows a "default" template to be used for any given action in a Rails app
Ruby
3
star
21

carat

A Ruby-like programming language written in Ruby
Ruby
3
star
22

scripts

Various little scripts I write
Ruby
3
star
23

active_record_query

Ruby
2
star
24

find_or_create_by

Rails plugin allowing records to be found or created based on an arbitrary hash of conditions
Ruby
1
star
25

phoenix_live_view-bug-demo

Elixir
1
star
26

vim-textobj-heredoc

Vim Script
1
star
27

PoTG

Java
1
star