Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with HTTPS or Subversion.

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