Controller Testing: You're Doing It Wrong
-
Upload
johnnygroundwork -
Category
Engineering
-
view
117 -
download
0
description
Transcript of Controller Testing: You're Doing It Wrong
Controller Testing
“You’re doing it wrong”
Jonathan Mukai-Heidt
Hello!
Groundwork
Let’s talk controller tests
Almost no one knows how to test controllers
Many, many, many different projects
Two years consulting at Pivotal Labs
Kicked around NYC start up scene
Freelance software developer
Controller Testing Hall of Shame
Wait, testing… why?
Catching regressions
Developing code in isolation!!!
Driving modular, composable code!!!
Back to the Hall of Shame
Stub all the things
describe "#show" do subject { -> { get :show, id: id } } let(:id) { '77' } let(:pizza) { Pizza.new }
context "with an existing pizza" do before { Pizza.should_receive(:find).with(id).and_return(pizza) } it { assigns(:pizza).should == pizza } end
context "with a non-existent pizza" do before { Pizza.should_receive(:find).with(id).and_raise_error(ActiveRecord::RecordNotFound) it { should raise_error(ActiveRecord::RecordNotFound) } endend
Everything is integration
As a userGiven there is a pepperoni pizzaWhen I visit the pizza index pageAnd I click on "pepperoni"Then I should see the pepperoni pizza
render_views
No tests at all…
Often the things that really matter are untested
Controllers often have a “big action” and “small details”
“Big” concerns should be the same!
Fetch or create/update a resource
“Small” concerns are actually very important
Require authentication?
Who is authorized?
What formats?
I was also confused
One day…
Rails controllers (+ responders) are awesomely declarative
What do we really mean when we say declarative
Imperative / Declarative
Describe the properties we want
No logic (really!)
Imperative“When deleting a user, if the current user is an admin user, then allow the deletion; if the current user is not an admin, do not allow the deletion to finish.”
Declarative“Only admin users can delete another user.”
Imperative“When a request for a resource comes in, if the request is for JSON, then fetch the resource and render it from the JSON template; if the request is for HTML, then fetch the resource and render the HTML template; if the request is for another format like PDF, return an error.”
Declarative“This controller returns a resource represented as JSON or HTML.”
Ruby is imperative but it lets us write declarative code
Look at how declarative Rails controllers can be
before_filter
# let's us do things like
before_filter :authenticate_user!, except: :show
before_filter
# ...or...
before_filter :load_some_model, except: [:new, :index]
Authorization
# Using Authority gem
authorize_actions_for SomeResource
Authorization
# Using CanCan gem
load_and_authorize_resource :some_resource
Rails 4 + Responders
respond_to
# quickly declare formats
respond_to :html, :json
SHOW
def new respond_with(@pizza)end
CREATE
def create respond_with(@pizza = Pizza.create(pizza_params))end
…and so on…
Little to no logic in controllers
And this is great!
But it’s not what 90% of the controllers I come across look like
Because of muddying these nice declarative controllers with business logic
Business logic belongs in modelsYou’ve heard this many times already
What do we really care about in controllers?
Authentication
Authorization
Presence of resourceWhat resource are we working with
Response
Tests should help us write better code
Declarative controller? Declarative tests!
What does it look like in action?
Shared examples cover the “small” details
“Big” actions can be simple…
# e.g. a show actionit { should assign(:some_resource) }
# e.g. a create actionit { should change(Pizza, :count).by(+1) }
Authentication
describe CommentsController do let(:current_user) { users(:claude) } let(:blog_post) { blog_posts(:top_ten_pizzas) }
describe "#new" do subject { -> { get :new, blog_post_id: blog_post } }
context "with a logged in user" do before { sign_in(:user, current_user) }
it "should not redirect to the login page" do response.should_not be_redirect end end
context "with an unauthenticated user" do it "should redirect to the login page" do response.should be_redirect_to(sign_in_path) end end endend
Authentication shared example
shared_examples_for "an action that requires a login" do before { sign_out :user } it { should respond_with_redirect_to(sign_in_path) }end
Authentication shared example in action
describe CommentsController do let(:current_user) { users(:claude) } let(:blog_post) { blog_posts(:top_ten_pizzas) }
before { sign_in(:user, current_user) }
describe "#new" do subject { -> { get :new, blog_post_id: blog_post } }
it_should_behave_like "an action that requires a login" endend
Authorization
describe "#create" do subject { -> { post :create, blog_post_id: blog_post, comment: params } }
let(:params) { { body: "What a great post. I loved the part about shared examples." } }
before { sign_in :user, current_user }
context "with an authorized user" do let(:current_user) { users(:bob) }
it "should respond with created" do response.should respond_with 201 end end
context "with an unauthorized user" do let(:current_user) { users(:mallory) }
it "should respond with 404" do response.should respond_with 404 end endend
“Malicious Mallory”
Authorization shared example
shared_examples_for "an action that requires authorization" do before { sign_in :user, users(:mallory) } it { should respond_with 404 }end
Authorization shared example in action
describe "#create" do subject { -> { post :create, blog_post_id: blog_post, comment: params } }
let(:params) { { body: "What a great post. I loved the part about shared examples." } }
before { sign_in :user, users(:bob) }
it_should_behave_like "a non-navigation action that requires a login" it_should_behave_like "an action that requires authorization"end
Presence of Resource
Presence shared example
shared_examples_for "an action that requires" do |*resources| resources.each do |resource| context "with an invalid or missing #{resource}" do let(resource) { double(to_param: "does-not-exist", reload: nil) } it { should respond_with 404 } end endend
Presence shared example in action
describe PizzaController do describe "#show" do subject { -> { get :show, id: pizza, format: format } } let(:pizza) { pizzas(:pepperoni) }
it_should_behave_like "an action that requires", :pizza endend
Response
Response shared example
shared_examples_for "an action that returns" do |*acceptable_formats| acceptable_formats.each do |acceptable_format| context "expecting a response in #{acceptable_format} format" do let(:format) { acceptable_format } it { should_not respond_with_status(:not_acceptable) } end end
(%i(html js json xml csv) - acceptable_formats.collect(&:to_sym)).each do |unacceptable_format| context "expecting a response in #{unacceptable_format} format" do let(:format) { unacceptable_format } it { should respond_with_status(:not_acceptable) } end endend
Response shared example in action
describe CommentsController do let(:current_user) { users(:claude) } let(:blog_post) { blog_posts(:top_ten_pizzas) } let(:format) { :html }
before { sign_in :user, current_user }
describe "#show" do subject { -> { get :show, id: comment, format: format } } let(:comment) { blog_post.comments.first }
it_should_behave_like "an action that returns", :html end
describe "#create" do subject { -> { post :create, blog_post_id: blog_post, comment: params, format: format } } let(:params) { { body: "What a great post. I loved the part about shared examples." } }
it_should_behave_like "an action that returns", :html, :json endend
Your test is like a check list
But what about…Likes/Bookmarks/Ratings
Bulk creates
Merging records
Actions that touch several models
“Skinny controller, fat model”
Ever since I began Rails work people have been saying this
5/6 projects suffer from bloated controllers
ActiveModel
Use it!
There is no resource too small
Models are cheap, especially ones not tied to the DB
An illustrative example
Password ResetClient wanted to overhaul a legacy password reset workflow
Suspend your dis-belief, they are not using Devise yet
Too simple to break out into a model?
Requirements always change“Ah but wait, we want to tell users if they put in their e-mail wrong.”
Of course requirements change again“If the user is locked out of their account, we shouldn’t send a password reset.”
Suddenly, a fat controller
ActiveModel makes it simple
Think nouns (resources), not verbs
HTTP gives you all the verbs you need
FootworkNo gem!
This will vary from project to project
Figure out how your project will handle these situations
Habbits
Hence the controller checklist
The rewards are great!
Easier to test
Drives good design
Keep controllers simple
Logic goes in models where it belongs
No confusion about where things go (bulk creates, likes, etc)
Uniform controllers == less dev time