Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with HTTPS or Subversion.

Download ZIP
Browse files

rework the readme

  • Loading branch information...
commit e14e452fd921975c730dd67d15a738a74cfb7362 1 parent 33d50ae
@jonleighton authored
Showing with 83 additions and 188 deletions.
  1. +83 −188 README.md
View
271 README.md
@@ -1,106 +1,71 @@
-# Focused Controller: Bringing Real OOP to Rails Controllers #
+# Focused Controller #
[![Build Status](https://secure.travis-ci.org/jonleighton/focused_controller.png?branch=master)](http://travis-ci.org/jonleighton/focused_controller)
-## Description ##
+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.
-Classical Rails controllers violate the Single Responsibility Principle.
-
-Each different "action" has separate responsibilities: A `create`
-action does something entirely different to a `destroy` action, yet
-they end up lumped into the same object.
-
-This has two unfortunate side effects:
-
-1. *We end up using instance variables to share data with our views when
- we should really be using methods*. Using instance variables for this
- purpose breaks encapsulation and can lead to subtle bugs. For example,
- an undeclared instance variable in a view will be nil, rather than
- raising an error when referenced.
-
-2. *We misuse before_filters to share functionality between actions*.
- Instead of using proper OO patterns like inheritance and mixins to keep
- our code DRY, we shoe-horn `before_filter` with `:only` or `:except` to
- share chunks of code between specific actions.
-
-A related problem with controllers is that the way we test them is slow
-and esoteric. Rather than calling a single method on our controller
-object and making assertions about what happened, we generate a request
-to put through the full controller stack, exercising large amounts of
-internal Rails code in each test case. These are not unit tests, yet
-they are often used to test logic.
-
-Focused Controller aims to address these issues by using a single, focused
-object for each action. These object should have only one responsibility
-and be straightforward to instantiate and test in isolation.
-
-## Feedback needed ##
-
-This project is in early stages, and while I have been using it
-successfully on a production application, I'm very keen for others to
-start experimenting with it and providing feedback.
-
-Note that I will follow SemVer, and the project is currently pre-1.0, so
-there could be API changes. However if the user base grows significantly,
-then I will try to avoid painful changes.
+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.
There is a [mailing list](http://groups.google.com/group/focused_controller)
for discussion.
-## Usage ##
-
-Focused Controller changes Rails' conventions. Rather than controllers
-being classes that contain one method per action, controllers are now
-namespaces and each action is a class within that namespace.
-
-Controllers which wish to use this convention include the
-`FocusedController::Mixin` module. This means you can start using
-Focused Controller in an existing project without having to rewrite all
-your existing controller code.
-
-An example:
+## Synopsis ##
``` ruby
+class ApplicationController
+ include FocusedController::Mixin
+end
+
module PostsController
- # Action is a common superclass for all the actions
- # inside `PostsController`.
- class Action < ApplicationController
- include FocusedController::Mixin
+ class Index < ApplicationController
+ expose(:posts) { Post.recent.limit(5) }
end
- class Index < Action
- def run
- # Your code here.
- end
-
- # No instance variables are shared with the view. Instead,
- # public methods are defined.
- def posts
- @posts ||= Post.all
- end
-
- # To prevent yourself having to write `controller.posts`
- # in the view, you can declare the method as a helper
- # method which means that calling `posts` automatically
- # delegates to the controller.
- helper_method :posts
+ class New < ApplicationController
+ expose(:post) { Post.new }
end
- # Actions do not need to declare a `run` method - the default
- # implementation inherited from `FocusedController::Mixin` is an
- # empty method.
- class Show < Action
- # Here's a shorter way to declare a method that is also a
- # helper_method
+ class Singular < ApplicationController
expose(:post) { Post.find params[:id] }
+ before_filter { redirect_to root_path unless post.accessible_to?(current_user) }
+ end
- # You can also call expose without a block, in which case an
- # attr_reader and a helper_method are declared
- expose :first_comment
+ class Show < Singular
+ end
+
+ class Update < Singular
+ def call
+ if post.update_attributes(params[:post])
+ # ...
+ else
+ # ...
+ end
+ end
end
end
```
+Some notes:
+
+* 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
+* `expose` makes the object returned by the block available in your view
+ template. It also memoizes the result so the block will only be
+ executed once.
+* It is not necessary to specify `:only` or `:except` on the before
+ filter, since we declare the filter for exactly the actions we want it
+ to run on. Rails has many methods which accept action names to limit
+ what they get applied to - this is basically irrelevant with Focused
+ Controller due to the increased granularity that having a class for each
+ action gives us.
+* The `#call` method is what gets invoked when the action runs, so put
+ the 'active ingredients' in here.
+
## Routing ##
Rails' normal routing assumes your actions are methods inside an object
@@ -122,13 +87,7 @@ Loco2::Application.routes.draw do
end
```
-The route will now map to `PostsController::New#run`.
-
-This is similar to writing:
-
-``` ruby
-get '/posts/new' => proc { |env| PostsController::New.call(env) }
-```
+The route will now map to `PostsController::New#call`.
All the normal routing macros are also supported:
@@ -140,14 +99,13 @@ end
## Functional Testing ##
-Though it's not encouraged, focused controllers can be tested in the
-classical 'functional' style. This can be a useful interim measure when
-converting a controller to be properly unit tested.
-
-It no longer makes sense to specify the action name to be called as the
-action name is always "run". So this is omitted:
+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:
``` ruby
+require 'focused_controller/functional_test_helper'
+
module UsersController
class CreateTest < ActionController::TestCase
include FocusedController::FunctionalTestHelper
@@ -166,6 +124,8 @@ end
There is also an equivalent helper for RSpec:
``` ruby
+require 'focused_controller/rspec_functional_helper'
+
describe UsersController do
include FocusedController::RSpecFunctionalHelper
@@ -178,18 +138,11 @@ describe UsersController do
end
```
-Don't forget to add the line below to your spec_helper.rb file:
-
-``` ruby
-require 'focused_controller/rspec_functional_helper'
-```
-
## Unit Testing ##
-A better way to test your controllers is with unit tests. This involves
-creating an instance of your action class and calling methods on it. For
-example, to test that your `user` method finds the correct user, you
-might write:
+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:
``` ruby
module UsersController
@@ -206,10 +159,10 @@ module UsersController
end
```
-### The `#run` method ###
+### The `#call` method ###
-Testing the code in your `#run` method is a little more involved,
-depending on what's in it. For example, your `#run` method may use
+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
@@ -223,21 +176,18 @@ 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.
-The fact that we have to do this is an indication of high coupling
-between the controller and these other objects. In the future, I want to
-look at ways to reduce this coupling and make testing more straightforward
-and obvious.
-
-In the mean time, here is an example:
-
``` ruby
+require 'focused_controller/test_helper'
+
module UsersController
class CreateTest < ActiveSupport::TestCase
include FocusedController::TestHelper
test "should create user" do
+ controller.params = { user: { name: 'Jon' } }
+
assert_difference('User.count') do
- req user: { name: 'Jon' }
+ controller.call
end
assert_redirected_to user_path(controller.user)
@@ -246,85 +196,50 @@ module UsersController
end
```
-### The `req` helper ###
-
-The `req` method runs the "request", but it does *not* go through the
-Rack stack. It simply sets up the params, session, flash, and then calls
-the `#run` method. The following are equivalent:
-
-``` ruby
-req({ x: 'x' }, { y: 'y' }, { z: 'z' })
-```
-
-``` ruby
-controller.params = { x: 'x' }
-session.update(y: 'y')
-flash.update(z: 'z')
-controller.run
-```
-
### Assertions ###
-You also have access to the normal assertions found in Rails' functional
-tests:
+You have access to the normal assertions found in Rails' functional tests:
* `assert_template`
* `assert_response`
* `assert_redirected_to`
-However, I intend to consider alternatives to these. For example,
-
-``` ruby
-assert_equal users_path, controller.location
-```
-
-seems lot more straightforward and explicit to me than:
-
-``` ruby
-assert_redirected_to users_path
-```
-
### Filters ###
-We're not testing through the Rack stack. We're just calling the `#run`
-method. Therefore, filters do not get run. This is a feature: if your
-filter code is truly orthogonal to your controller code it should be
-unit tested separately. If it is not orthogonal then you should find a
-way to invoke it more explicitly than via filters.
-
-(At this point I will ask: if it is truly orthogonal, why not make it a
-Rack middleware?)
+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:
``` ruby
+require 'focused_controller/rspec_helper'
+
describe UsersController do
include FocusedController::RSpecHelper
describe UsersController::Create do
test "should create user" do
- expect { req user: { name: 'Jon' } }.to change(User, :count).by(1)
+ 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
```
-Don't forget to add the line below to your spec_helper.rb file:
-
-``` ruby
-require 'focused_controller/rspec_helper'
-```
-
## Isolated unit tests ##
-It is possible to completely decouple your focused controller tests from
-the Rails application. This means you don't have to pay the penalty of
-starting up Rails every time you want to run a test. The benefit this
-brings will depend on how coupled your controllers/tests are to other
-dependencies.
+It is possible to completely decouple your focused controller tests from the
+Rails application. This means you don't have to pay the penalty of starting up
+Rails every time you want to run a test. This is an advanced feature and your
+mileage may vary. The benefit this brings will depend on how coupled your
+controllers/tests are to other dependencies.
Your `config/routes.rb` file is a dependency. When you use a URL helper
you are depending on that file. As this is a common dependency, Focused
@@ -346,26 +261,6 @@ methods in your test and your controller return stub objects. These can
be compared, so `user_path(user1) == user_path(user1)`, but
`user_path(user1) != user_path(user2)`.
-## Speed comparison ##
-
-Here's a comparison of running the same test in each of the different
-styles:
-
-### Functional ###
-
-* **Test time**: 0.154842s, 45.2075 tests/s, 64.5821 assertions/s
-* **Total time**: 3.380s
-
-### Unit ###
-
-* **Test time**: 0.046101s, 151.8393 tests/s, 216.9133 assertions/s
-* **Total time**: 3.578s
-
-### Isolated Unit ###
-
-* **Test time**: 0.016669s, 419.9434 tests/s, 599.9191 assertions/s
-* **Total time**: 2.398s
-
## More examples ##
The [acceptance
@@ -376,4 +271,4 @@ 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, necessarily.)
+would typically write controllers and tests.)
Please sign in to comment.
Something went wrong with that request. Please try again.