Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with HTTPS or Subversion.

Download ZIP
Newer
Older
100644 380 lines (283 sloc) 10.816 kb
dce9389 @tekin More minor tweaks
tekin authored
1 # Focused Controller: Bringing Real OOP to Rails Controllers #
3def6e6 @jonleighton Initial pass at a README
authored
2
b31675e @jonleighton travis build status
authored
3 [![Build Status](https://secure.travis-ci.org/jonleighton/focused_controller.png?branch=master)](http://travis-ci.org/jonleighton/focused_controller)
3def6e6 @jonleighton Initial pass at a README
authored
4
5 ## Description ##
6
7 Classical Rails controllers violate the Single Responsibility Principle.
8
dce9389 @tekin More minor tweaks
tekin authored
9 Each different "action" has separate responsibilities: A `create`
ad66142 @tekin Reword opening paragraph
tekin authored
10 action does something entirely different to a `destroy` action, yet
11 they end up lumped into the same object.
3def6e6 @jonleighton Initial pass at a README
authored
12
3789042 @jonleighton some edits to @tekin's changes
authored
13 This has two unfortunate side effects:
3def6e6 @jonleighton Initial pass at a README
authored
14
cdcda94 @tekin Reword side effect No. 1
tekin authored
15 1. *We end up using instance variables to share data with our views when
16 we should really be using methods*. Using instance variables for this
3789042 @jonleighton some edits to @tekin's changes
authored
17 purpose breaks encapsulation and can lead to subtle bugs. For example,
18 an undeclared instance variable in a view will be nil, rather than
19 raising an error when referenced.
3def6e6 @jonleighton Initial pass at a README
authored
20
dce9389 @tekin More minor tweaks
tekin authored
21 2. *We misuse before_filters to share functionality between actions*.
b7edfc9 @tekin More tweaks to side effect No. 2
tekin authored
22 Instead of using proper OO patterns like inheritance and mixins to keep
23 our code DRY, we shoe-horn `before_filter` with `:only` or `:except` to
dce9389 @tekin More minor tweaks
tekin authored
24 share chunks of code between specific actions.
3def6e6 @jonleighton Initial pass at a README
authored
25
3789042 @jonleighton some edits to @tekin's changes
authored
26 A related problem with controllers is that the way we test them is slow
27 and esoteric. Rather than calling a single method on our controller
28 object and making assertions about what happened, we generate a request
29 to put through the full controller stack, exercising large amounts of
30 internal Rails code in each test case. These are not unit tests, yet
31 they are often used to test logic.
3def6e6 @jonleighton Initial pass at a README
authored
32
3789042 @jonleighton some edits to @tekin's changes
authored
33 Focused Controller aims to address these issues by using a single, focused
34 object for each action. These object should have only one responsibility
35 and be straightforward to instantiate and test in isolation.
3def6e6 @jonleighton Initial pass at a README
authored
36
37 ## Feedback needed ##
38
39 This project is in early stages, and while I have been using it
40 successfully on a production application, I'm very keen for others to
41 start experimenting with it and providing feedback.
42
43 Note that I will follow SemVer, and the project is currently pre-1.0, so
2a848cb @tekin reword
tekin authored
44 there could be API changes. However if the user base grows significantly,
45 then I will try to avoid painful changes.
3def6e6 @jonleighton Initial pass at a README
authored
46
7793d4c @jonleighton Update README.md
authored
47 There is a [mailing list](http://groups.google.com/group/focused_controller)
48 for discussion.
9b8a6d9 @shingara Add link about google group to speak on focused_controller
shingara authored
49
3def6e6 @jonleighton Initial pass at a README
authored
50 ## Usage ##
51
52 Focused Controller changes Rails' conventions. Rather than controllers
dce9389 @tekin More minor tweaks
tekin authored
53 being classes that contain one method per action, controllers are now
3789042 @jonleighton some edits to @tekin's changes
authored
54 namespaces and each action is a class within that namespace.
55
56 Controllers which wish to use this convention include the
57 `FocusedController::Mixin` module. This means you can start using
bc6c959 @tekin Reword usage
tekin authored
58 Focused Controller in an existing project without having to rewrite all
59 your existing controller code.
3def6e6 @jonleighton Initial pass at a README
authored
60
61 An example:
62
63 ``` ruby
64 module PostsController
dce9389 @tekin More minor tweaks
tekin authored
65 # Action is a common superclass for all the actions
3def6e6 @jonleighton Initial pass at a README
authored
66 # inside `PostsController`.
67 class Action < ApplicationController
68 include FocusedController::Mixin
69 end
70
71 class Index < Action
72 def run
3789042 @jonleighton some edits to @tekin's changes
authored
73 # Your code here.
3def6e6 @jonleighton Initial pass at a README
authored
74 end
75
76 # No instance variables are shared with the view. Instead,
77 # public methods are defined.
78 def posts
79 @posts ||= Post.all
80 end
81
82 # To prevent yourself having to write `controller.posts`
83 # in the view, you can declare the method as a helper
84 # method which means that calling `posts` automatically
85 # delegates to the controller.
86 helper_method :posts
87 end
88
89 # Actions do not need to declare a `run` method - the default
90 # implementation inherited from `FocusedController::Mixin` is an
91 # empty method.
92 class Show < Action
3a9b346 @jonleighton add #expose
authored
93 # Here's a shorter way to declare a method that is also a
94 # helper_method
95 expose(:post) { Post.find params[:id] }
96
97 # You can also call expose without a block, in which case an
98 # attr_reader and a helper_method are declared
99 expose :first_comment
3def6e6 @jonleighton Initial pass at a README
authored
100 end
101 end
102 ```
103
104 ## Routing ##
105
dce9389 @tekin More minor tweaks
tekin authored
106 Rails' normal routing assumes your actions are methods inside an object
107 whose name ends with 'controller'. For example:
3def6e6 @jonleighton Initial pass at a README
authored
108
109 ``` ruby
110 get '/posts/new' => 'posts#new'
111 ```
112
113 will route `GET /posts/new` to `PostsController#new`.
114
f38aaa3 @tekin Tweak example code
tekin authored
115 To get around this, we use the `focused_controller_routes` helper:
3def6e6 @jonleighton Initial pass at a README
authored
116
117 ``` ruby
6ac073f @jonleighton Improve routing example. Fixes #15.
authored
118 Loco2::Application.routes.draw do
119 focused_controller_routes do
120 get '/posts/new' => 'posts#new'
121 end
3def6e6 @jonleighton Initial pass at a README
authored
122 end
123 ```
124
125 The route will now map to `PostsController::New#run`.
126
127 This is similar to writing:
128
129 ``` ruby
130 get '/posts/new' => proc { |env| PostsController::New.call(env) }
131 ```
132
dce9389 @tekin More minor tweaks
tekin authored
133 All the normal routing macros are also supported:
3def6e6 @jonleighton Initial pass at a README
authored
134
135 ``` ruby
136 focused_controller_routes do
137 resources :posts
138 end
139 ```
140
141 ## Functional Testing ##
142
143 Though it's not encouraged, focused controllers can be tested in the
144 classical 'functional' style. This can be a useful interim measure when
145 converting a controller to be properly unit tested.
146
147 It no longer makes sense to specify the action name to be called as the
148 action name is always "run". So this is omitted:
149
150 ``` ruby
151 module UsersController
152 class CreateTest < ActionController::TestCase
153 include FocusedController::FunctionalTestHelper
154
155 test "should create user" do
156 assert_difference('User.count') do
157 post user: { name: 'Jon' }
158 end
159
160 assert_redirected_to user_path(@controller.user)
161 end
162 end
163 end
164 ```
165
166 There is also an equivalent helper for RSpec:
167
168 ``` ruby
169 describe UsersController do
170 include FocusedController::RSpecFunctionalHelper
171
172 describe UsersController::Create do
173 it "should create user" do
174 expect { post user: { name: 'Jon' } }.to change(User, :count).by(1)
175 response.should redirect_to(user_path(subject.user))
176 end
177 end
178 end
179 ```
180
84e7e09 @dyba Updated README testing section
dyba authored
181 Don't forget to add the line below to your spec_helper.rb file:
182
183 ``` ruby
184 require 'focused_controller/rspec_functional_helper'
185 ```
186
3def6e6 @jonleighton Initial pass at a README
authored
187 ## Unit Testing ##
188
189 A better way to test your controllers is with unit tests. This involves
dce9389 @tekin More minor tweaks
tekin authored
190 creating an instance of your action class and calling methods on it. For
3def6e6 @jonleighton Initial pass at a README
authored
191 example, to test that your `user` method finds the correct user, you
192 might write:
193
194 ``` ruby
195 module UsersController
196 class ShowTest < ActiveSupport::TestCase
197 test 'finds the user' do
198 user = User.create
199
200 controller = UsersController::Show.new
201 controller.params = { id: user.id }
202
203 assert_equal user, controller.user
204 end
205 end
206 end
207 ```
208
209 ### The `#run` method ###
210
211 Testing the code in your `#run` method is a little more involved,
212 depending on what's in it. For example, your `#run` method may use
213 (explicitly or implicitly) any of the following objects:
214
215 * request
216 * response
217 * params
218 * session
219 * flash
220 * cookies
221
222 To make the experience smoother, Focused Controller sets up mock
223 versions of these objects, much like with classical functional testing.
f38aaa3 @tekin Tweak example code
tekin authored
224 It also provides accessors for these objects in your test class.
3def6e6 @jonleighton Initial pass at a README
authored
225
226 The fact that we have to do this is an indication of high coupling
227 between the controller and these other objects. In the future, I want to
f38aaa3 @tekin Tweak example code
tekin authored
228 look at ways to reduce this coupling and make testing more straightforward
229 and obvious.
3def6e6 @jonleighton Initial pass at a README
authored
230
231 In the mean time, here is an example:
232
233 ``` ruby
234 module UsersController
235 class CreateTest < ActiveSupport::TestCase
236 include FocusedController::TestHelper
237
238 test "should create user" do
239 assert_difference('User.count') do
240 req user: { name: 'Jon' }
241 end
242
243 assert_redirected_to user_path(controller.user)
244 end
245 end
246 end
247 ```
248
249 ### The `req` helper ###
250
251 The `req` method runs the "request", but it does *not* go through the
252 Rack stack. It simply sets up the params, session, flash, and then calls
253 the `#run` method. The following are equivalent:
254
255 ``` ruby
256 req({ x: 'x' }, { y: 'y' }, { z: 'z' })
257 ```
258
259 ``` ruby
260 controller.params = { x: 'x' }
261 session.update(y: 'y')
262 flash.update(z: 'z')
263 controller.run
264 ```
265
266 ### Assertions ###
267
268 You also have access to the normal assertions found in Rails' functional
269 tests:
270
271 * `assert_template`
272 * `assert_response`
273 * `assert_redirected_to`
274
275 However, I intend to consider alternatives to these. For example,
276
277 ``` ruby
278 assert_equal users_path, controller.location
279 ```
280
281 seems lot more straightforward and explicit to me than:
282
283 ``` ruby
284 assert_redirected_to users_path
285 ```
286
287 ### Filters ###
288
289 We're not testing through the Rack stack. We're just calling the `#run`
290 method. Therefore, filters do not get run. This is a feature: if your
291 filter code is truly orthogonal to your controller code it should be
292 unit tested separately. If it is not orthogonal then you should find a
293 way to invoke it more explicitly than via filters.
294
295 (At this point I will ask: if it is truly orthogonal, why not make it a
296 Rack middleware?)
297
298 ### RSpec ###
299
300 There is a helper for RSpec as well:
301
302 ``` ruby
303 describe UsersController do
304 include FocusedController::RSpecHelper
305
306 describe UsersController::Create do
307 test "should create user" do
308 expect { req user: { name: 'Jon' } }.to change(User, :count).by(1)
309 response.should redirect_to(user_path(subject.user))
310 end
311 end
312 end
313 ```
314
84e7e09 @dyba Updated README testing section
dyba authored
315 Don't forget to add the line below to your spec_helper.rb file:
316
317 ``` ruby
318 require 'focused_controller/rspec_helper'
319 ```
320
3def6e6 @jonleighton Initial pass at a README
authored
321 ## Isolated unit tests ##
322
f38aaa3 @tekin Tweak example code
tekin authored
323 It is possible to completely decouple your focused controller tests from
324 the Rails application. This means you don't have to pay the penalty of
3def6e6 @jonleighton Initial pass at a README
authored
325 starting up Rails every time you want to run a test. The benefit this
326 brings will depend on how coupled your controllers/tests are to other
327 dependencies.
328
329 Your `config/routes.rb` file is a dependency. When you use a URL helper
330 you are depending on that file. As this is a common dependency, Focused
331 Controller provides a way to stub out URL helpers:
332
333 ``` ruby
334 module UsersController
335 class CreateTest < ActiveSupport::TestCase
336 include FocusedController::TestHelper
337 stub_url :user
338
339 # ...
340 end
341 end
342 ```
343
344 The `stub_url` declaration will make the `user_path` and `user_url`
345 methods in your test and your controller return stub objects. These can
346 be compared, so `user_path(user1) == user_path(user1)`, but
347 `user_path(user1) != user_path(user2)`.
348
a13de2a @jonleighton improve the speed comparison
authored
349 ## Speed comparison ##
350
351 Here's a comparison of running the same test in each of the different
352 styles:
353
354 ### Functional ###
355
356 * **Test time**: 0.154842s, 45.2075 tests/s, 64.5821 assertions/s
357 * **Total time**: 3.380s
358
359 ### Unit ###
360
361 * **Test time**: 0.046101s, 151.8393 tests/s, 216.9133 assertions/s
362 * **Total time**: 3.578s
363
364 ### Isolated Unit ###
365
366 * **Test time**: 0.016669s, 419.9434 tests/s, 599.9191 assertions/s
367 * **Total time**: 2.398s
368
3def6e6 @jonleighton Initial pass at a README
authored
369 ## More examples ##
370
371 The [acceptance
89638b0 @andyw8 Fixing links in README
andyw8 authored
372 tests](https://github.com/jonleighton/focused_controller/tree/master/test/acceptance)
3def6e6 @jonleighton Initial pass at a README
authored
373 for Focused Controller exercise a [complete Rails
89638b0 @andyw8 Fixing links in README
andyw8 authored
374 application](https://github.com/jonleighton/focused_controller/tree/master/test/app),
3def6e6 @jonleighton Initial pass at a README
authored
375 which uses the plugin. Therefore, you might wish to look there to get
376 more of an idea about how it can be used.
377
378 (Note that the code there is based on Rails' scaffolding, not how I
379 would typically write controllers and tests, necessarily.)
Something went wrong with that request. Please try again.