Successfully reported this slideshow.
We use your LinkedIn profile and activity data to personalize ads and to show you more relevant ads. You can change your ad preferences anytime.

Controller Testing: You're Doing It Wrong

809 views

Published on

Talk for RubyKaig 2014

Published in: Engineering
  • D0WNL0AD FULL ▶ ▶ ▶ ▶ http://1lite.top/DdXw6 ◀ ◀ ◀ ◀
       Reply 
    Are you sure you want to  Yes  No
    Your message goes here

Controller Testing: You're Doing It Wrong

  1. 1. Controller Testing “You’re doing it wrong” Jonathan Mukai-Heidt
  2. 2. Hello!
  3. 3. Groundwork
  4. 4. Let’s talk controller tests
  5. 5. Almost no one knows how to test controllers
  6. 6. Many, many, many different projects Two years consulting at Pivotal Labs Kicked around NYC start up scene Freelance software developer
  7. 7. Controller Testing Hall of Shame
  8. 8. Wait, testing… why?
  9. 9. Catching regressions
  10. 10. Developing code in isolation!!!
  11. 11. Driving modular, composable code!!!
  12. 12. Back to the Hall of Shame
  13. 13. 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) } end end
  14. 14. Everything is integration As a user Given there is a pepperoni pizza When I visit the pizza index page And I click on "pepperoni" Then I should see the pepperoni pizza
  15. 15. render_views
  16. 16. No tests at all …
  17. 17. Often the things that really matter are untested
  18. 18. Controllers often have a “big action” and “small details”
  19. 19. “Big” concerns should be the same! Fetch or create/update a resource
  20. 20. “Small” concerns are actually very important Require authentication? Who is authorized? What formats?
  21. 21. I was also confused
  22. 22. One day…
  23. 23. Rails controllers (+ responders) are awesomely declarative
  24. 24. What do we really mean when we say declarative
  25. 25. Imperative / Declarative
  26. 26. Describe the properties we want
  27. 27. No logic (really!)
  28. 28. 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.”
  29. 29. Declarative “Only admin users can delete another user.”
  30. 30. 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.”
  31. 31. Declarative “This controller returns a resource represented as JSON or HTML.”
  32. 32. Ruby is imperative but it lets us write declarative code
  33. 33. Look at how declarative Rails controllers can be
  34. 34. before_filter # let's us do things like before_filter :authenticate_user!, except: :show
  35. 35. before_filter # ...or... before_filter :load_some_model, except: [:new, :index]
  36. 36. Authorization # Using Authority gem authorize_actions_for SomeResource
  37. 37. Authorization # Using CanCan gem load_and_authorize_resource :some_resource
  38. 38. Rails 4 + Responders
  39. 39. respond_to # quickly declare formats respond_to :html, :json
  40. 40. SHOW def new respond_with(@pizza) end
  41. 41. CREATE def create respond_with(@pizza = Pizza.create(pizza_params)) end
  42. 42. …and so on…
  43. 43. Little to no logic in controllers
  44. 44. And this is great!
  45. 45. But it’s not what 90% of the controllers I come across look like
  46. 46. Because of muddying these nice declarative controllers with business logic
  47. 47. Business logic belongs in models You’ve heard this many times already
  48. 48. What do we really care about in controllers?
  49. 49. Authentication
  50. 50. Authorization
  51. 51. Presence of resource What resource are we working with
  52. 52. Response
  53. 53. Tests should help us write better code
  54. 54. Declarative controller? Declarative tests!
  55. 55. What does it look like in action?
  56. 56. Shared examples cover the “small” details
  57. 57. “Big” actions can be simple… # e.g. a show action it { should assign(:some_resource) } # e.g. a create action it { should change(Pizza, :count).by(+1) }
  58. 58. 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 end end
  59. 59. 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
  60. 60. 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" end end
  61. 61. 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 end end
  62. 62. “Malicious Mallory”
  63. 63. Authorization shared example shared_examples_for "an action that requires authorization" do before { sign_in :user, users(:mallory) } it { should respond_with 404 } end
  64. 64. 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
  65. 65. Presence of Resource
  66. 66. 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 end end
  67. 67. 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 end end
  68. 68. Response
  69. 69. 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 end end
  70. 70. 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 end end
  71. 71. Your test is like a check list
  72. 72. But what about… Likes/Bookmarks/Ratings Bulk creates Merging records Actions that touch several models
  73. 73. “Skinny controller, fat model” Ever since I began Rails work people have been saying this
  74. 74. 5/6 projects suffer from bloated controllers
  75. 75. ActiveModel
  76. 76. Use it!
  77. 77. There is no resource too small
  78. 78. Models are cheap, especially ones not tied to the DB
  79. 79. An illustrative example
  80. 80. Password Reset Client wanted to overhaul a legacy password reset workflow
  81. 81. Suspend your dis-belief, they are not using Devise yet
  82. 82. Too simple to break out into a model?
  83. 83. Requirements always change “Ah but wait, we want to tell users if they put in their e-mail wrong.”
  84. 84. Of course requirements change again “If the user is locked out of their account, we shouldn’t send a password reset.”
  85. 85. Suddenly, a fat controller
  86. 86. ActiveModel makes it simple
  87. 87. Think nouns (resources), not verbs
  88. 88. HTTP gives you all the verbs you need
  89. 89. Footwork No gem! This will vary from project to project Figure out how your project will handle these situations
  90. 90. Habbits
  91. 91. Hence the controller checklist
  92. 92. The rewards are great!
  93. 93. Easier to test
  94. 94. Drives good design
  95. 95. Keep controllers simple
  96. 96. Logic goes in models where it belongs
  97. 97. No confusion about where things go (bulk creates, likes, etc)
  98. 98. Uniform controllers == less dev time
  99. 99. Thanks! Get in touch! Jonathan Mukai-Heidt Groundwork @johnnymukai johnny@buildgroundwork.co m

×