99
Controller Testing “You’re doing it wrong” Jonathan Mukai-Heidt

Controller Testing: You're Doing It Wrong

Embed Size (px)

DESCRIPTION

Talk for RubyKaig 2014

Citation preview

Page 1: Controller Testing: You're Doing It Wrong

Controller Testing

“You’re doing it wrong”

Jonathan Mukai-Heidt

Page 2: Controller Testing: You're Doing It Wrong

Hello!

Page 3: Controller Testing: You're Doing It Wrong

Groundwork

Page 4: Controller Testing: You're Doing It Wrong

Let’s talk controller tests

Page 5: Controller Testing: You're Doing It Wrong

Almost no one knows how to test controllers

Page 6: Controller Testing: You're Doing It Wrong

Many, many, many different projects

Two years consulting at Pivotal Labs

Kicked around NYC start up scene

Freelance software developer

Page 7: Controller Testing: You're Doing It Wrong

Controller Testing Hall of Shame

Page 8: Controller Testing: You're Doing It Wrong

Wait, testing… why?

Page 9: Controller Testing: You're Doing It Wrong

Catching regressions

Page 10: Controller Testing: You're Doing It Wrong

Developing code in isolation!!!

Page 11: Controller Testing: You're Doing It Wrong

Driving modular, composable code!!!

Page 12: Controller Testing: You're Doing It Wrong

Back to the Hall of Shame

Page 13: Controller Testing: You're Doing It Wrong

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

Page 14: Controller Testing: You're Doing It Wrong

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

Page 15: Controller Testing: You're Doing It Wrong

render_views

Page 16: Controller Testing: You're Doing It Wrong

No tests at all…

Page 17: Controller Testing: You're Doing It Wrong

Often the things that really matter are untested

Page 18: Controller Testing: You're Doing It Wrong

Controllers often have a “big action” and “small details”

Page 19: Controller Testing: You're Doing It Wrong

“Big” concerns should be the same!

Fetch or create/update a resource

Page 20: Controller Testing: You're Doing It Wrong

“Small” concerns are actually very important

Require authentication?

Who is authorized?

What formats?

Page 21: Controller Testing: You're Doing It Wrong

I was also confused

Page 22: Controller Testing: You're Doing It Wrong

One day…

Page 23: Controller Testing: You're Doing It Wrong

Rails controllers (+ responders) are awesomely declarative

Page 24: Controller Testing: You're Doing It Wrong

What do we really mean when we say declarative

Page 25: Controller Testing: You're Doing It Wrong

Imperative / Declarative

Page 26: Controller Testing: You're Doing It Wrong

Describe the properties we want

Page 27: Controller Testing: You're Doing It Wrong

No logic (really!)

Page 28: Controller Testing: You're Doing It Wrong

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.”

Page 29: Controller Testing: You're Doing It Wrong

Declarative“Only admin users can delete another user.”

Page 30: Controller Testing: You're Doing It Wrong

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.”

Page 31: Controller Testing: You're Doing It Wrong

Declarative“This controller returns a resource represented as JSON or HTML.”

Page 32: Controller Testing: You're Doing It Wrong

Ruby is imperative but it lets us write declarative code

Page 33: Controller Testing: You're Doing It Wrong

Look at how declarative Rails controllers can be

Page 34: Controller Testing: You're Doing It Wrong

before_filter

# let's us do things like

before_filter :authenticate_user!, except: :show

Page 35: Controller Testing: You're Doing It Wrong

before_filter

# ...or...

before_filter :load_some_model, except: [:new, :index]

Page 36: Controller Testing: You're Doing It Wrong

Authorization

# Using Authority gem

authorize_actions_for SomeResource

Page 37: Controller Testing: You're Doing It Wrong

Authorization

# Using CanCan gem

load_and_authorize_resource :some_resource

Page 38: Controller Testing: You're Doing It Wrong

Rails 4 + Responders

Page 39: Controller Testing: You're Doing It Wrong

respond_to

# quickly declare formats

respond_to :html, :json

Page 40: Controller Testing: You're Doing It Wrong

SHOW

def new respond_with(@pizza)end

Page 41: Controller Testing: You're Doing It Wrong

CREATE

def create respond_with(@pizza = Pizza.create(pizza_params))end

Page 42: Controller Testing: You're Doing It Wrong

…and so on…

Page 43: Controller Testing: You're Doing It Wrong

Little to no logic in controllers

Page 44: Controller Testing: You're Doing It Wrong

And this is great!

Page 45: Controller Testing: You're Doing It Wrong

But it’s not what 90% of the controllers I come across look like

Page 46: Controller Testing: You're Doing It Wrong

Because of muddying these nice declarative controllers with business logic

Page 47: Controller Testing: You're Doing It Wrong

Business logic belongs in modelsYou’ve heard this many times already

Page 48: Controller Testing: You're Doing It Wrong

What do we really care about in controllers?

Page 49: Controller Testing: You're Doing It Wrong

Authentication

Page 50: Controller Testing: You're Doing It Wrong

Authorization

Page 51: Controller Testing: You're Doing It Wrong

Presence of resourceWhat resource are we working with

Page 52: Controller Testing: You're Doing It Wrong

Response

Page 53: Controller Testing: You're Doing It Wrong

Tests should help us write better code

Page 54: Controller Testing: You're Doing It Wrong

Declarative controller? Declarative tests!

Page 55: Controller Testing: You're Doing It Wrong

What does it look like in action?

Page 56: Controller Testing: You're Doing It Wrong

Shared examples cover the “small” details

Page 57: Controller Testing: You're Doing It Wrong

“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) }

Page 58: Controller Testing: You're Doing It Wrong

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

Page 59: Controller Testing: You're Doing It Wrong

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

Page 60: Controller Testing: You're Doing It Wrong

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

Page 61: Controller Testing: You're Doing It Wrong

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

Page 62: Controller Testing: You're Doing It Wrong

“Malicious Mallory”

Page 63: Controller Testing: You're Doing It Wrong

Authorization shared example

shared_examples_for "an action that requires authorization" do before { sign_in :user, users(:mallory) } it { should respond_with 404 }end

Page 64: Controller Testing: You're Doing It Wrong

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

Page 65: Controller Testing: You're Doing It Wrong

Presence of Resource

Page 66: Controller Testing: You're Doing It Wrong

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

Page 67: Controller Testing: You're Doing It Wrong

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

Page 68: Controller Testing: You're Doing It Wrong

Response

Page 69: Controller Testing: You're Doing It Wrong

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

Page 70: Controller Testing: You're Doing It Wrong

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

Page 71: Controller Testing: You're Doing It Wrong

Your test is like a check list

Page 72: Controller Testing: You're Doing It Wrong

But what about…Likes/Bookmarks/Ratings

Bulk creates

Merging records

Actions that touch several models

Page 73: Controller Testing: You're Doing It Wrong

“Skinny controller, fat model”

Ever since I began Rails work people have been saying this

Page 74: Controller Testing: You're Doing It Wrong

5/6 projects suffer from bloated controllers

Page 75: Controller Testing: You're Doing It Wrong

ActiveModel

Page 76: Controller Testing: You're Doing It Wrong

Use it!

Page 77: Controller Testing: You're Doing It Wrong

There is no resource too small

Page 78: Controller Testing: You're Doing It Wrong

Models are cheap, especially ones not tied to the DB

Page 79: Controller Testing: You're Doing It Wrong

An illustrative example

Page 80: Controller Testing: You're Doing It Wrong

Password ResetClient wanted to overhaul a legacy password reset workflow

Page 81: Controller Testing: You're Doing It Wrong

Suspend your dis-belief, they are not using Devise yet

Page 82: Controller Testing: You're Doing It Wrong

Too simple to break out into a model?

Page 83: Controller Testing: You're Doing It Wrong

Requirements always change“Ah but wait, we want to tell users if they put in their e-mail wrong.”

Page 84: Controller Testing: You're Doing It Wrong

Of course requirements change again“If the user is locked out of their account, we shouldn’t send a password reset.”

Page 85: Controller Testing: You're Doing It Wrong

Suddenly, a fat controller

Page 86: Controller Testing: You're Doing It Wrong

ActiveModel makes it simple

Page 87: Controller Testing: You're Doing It Wrong

Think nouns (resources), not verbs

Page 88: Controller Testing: You're Doing It Wrong

HTTP gives you all the verbs you need

Page 89: Controller Testing: You're Doing It Wrong

FootworkNo gem!

This will vary from project to project

Figure out how your project will handle these situations

Page 90: Controller Testing: You're Doing It Wrong

Habbits

Page 91: Controller Testing: You're Doing It Wrong

Hence the controller checklist

Page 92: Controller Testing: You're Doing It Wrong

The rewards are great!

Page 93: Controller Testing: You're Doing It Wrong

Easier to test

Page 94: Controller Testing: You're Doing It Wrong

Drives good design

Page 95: Controller Testing: You're Doing It Wrong

Keep controllers simple

Page 96: Controller Testing: You're Doing It Wrong

Logic goes in models where it belongs

Page 97: Controller Testing: You're Doing It Wrong

No confusion about where things go (bulk creates, likes, etc)

Page 98: Controller Testing: You're Doing It Wrong

Uniform controllers == less dev time

Page 99: Controller Testing: You're Doing It Wrong

Thanks!Get in touch!

Jonathan Mukai-Heidt

Groundwork

@johnnymukai

[email protected]