Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with HTTPS or Subversion.

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