Upgrade to Pro — share decks privately, control downloads, hide ads and more …

Rails Testing Anti-Patterns (Confoo 2014)

Rails Testing Anti-Patterns (Confoo 2014)

Testing in our applications is essential to inspire confidence, catch regressions, and encourage simple designs. However, since tests have become such a large percentage of our applications, they're also prone to various code smells and anti-patterns. In this talk, we'll discuss various strategies to combat common testing mistakes found in Rails applications that reduce test code quality, performance, reliability, and maintainability.

Kevin Faustino

February 28, 2014
Tweet

More Decks by Kevin Faustino

Other Decks in Technology

Transcript

  1. What is an Anti-Pattern? –Gerard Meszaros “A pattern that shouldn’t

    be used because it is known to produce less than optimal results.”
  2. Integration with RSpec # Gemfile! ! group :development do! gem

    'spring', '~> 1.1.0'! gem 'spring-commands-rspec', require: false! end!
  3. require 'spec_helper'! ! describe MailerPresenter do! describe '#sender_identifier' do! subject(:presenter)

    { MailerPresenter.new(sender, double(:recipient)) }! ! describe '#sender_identifier' do! context 'sender name not set' do! let(:sender) { double(:sender, name: nil, email: '[email protected]') }! ! it 'returns the email address of the sender' do! expect(presenter.sender_identifier).to eq(sender.email)! end! end! ! context 'sender name set' do! let(:sender) {! double(:sender, name: 'Lex Luthor', email: '[email protected]')! }! ! it 'returns name of sender' do! expect(presenter.sender_identifier).to eq(sender.name)! end! end! end! end! end!
  4. class MailerPresenter! attr_reader :recipient, :sender! ! def initialize(sender, recipient)! @sender

    = sender! @recipient = recipient! end! ! def sender_identifier! sender.name || sender.email! end! end!
  5. require_relative '../../app/presenters/mailer_presenter'! ! describe MailerPresenter do! describe '#sender_identifier' do! subject(:presenter)

    { MailerPresenter.new(sender, double(:recipient)) }! ! describe '#sender_identifier' do! context 'sender name not set' do! let(:sender) { double(:sender, name: nil, email: '[email protected]') }! ! it 'returns the email address of the sender' do! expect(presenter.sender_identifier).to eq(sender.email)! end! end! ! context 'sender name set' do! let(:sender) {! double(:sender, name: 'Lex Luthor', email: '[email protected]')! }! ! it 'returns name of sender' do! expect(presenter.sender_identifier).to eq(sender.name)! end! end! end! end! end!
  6. SPec That Creates A Record require 'spec_helper'! ! describe User

    do! describe '#full_name' do! subject(:user) {! User.create(first_name: 'Bruce', last_name: 'Wayne') ! }! ! it 'returns combined first name and last name of the user' do! expect(user.full_name).to eq('Bruce Wayne')! end! end! end!
  7. SPec That Builds A Record require 'spec_helper'! ! describe User

    do! describe '#full_name' do! subject(:user) {! User.new(first_name: 'Bruce', last_name: 'Wayne') ! }! ! it 'returns combined first name and last name of the user' do! expect(user.full_name).to eq('Bruce Wayne')! end! end! end!
  8. Verifies too much in a Single Test describe PostsController do!

    describe 'GET #show' do! let!(:post) { build_stubbed(:post) }! ! it 'gets show action' do! post_class = class_double('Post').as_stubbed_const! expect(post_class)! ! ! ! .to receive(:find).with(post.id).and_return(post)! ! ! ! get :show, id: post.id! expect(response.status).to eq(200)! expect(assigns(:post)).to eq(post)! expect(response).to render_template(:new)! end! end! end!
  9. Verify a single condition per Test describe PostsController do! describe

    'GET #show' do! let!(:post) { build_stubbed(:post) }! ! before do! post_class = class_double('Post').as_stubbed_const! expect(post_class).to receive(:find).with(post.id).and_return(post)! ! get :show, id: post.id! end! ! it 'renders the :show template' do! expect(response).to render_template(:new)! end! ! it 'assigns post' do! expect(assigns(:post)).to eq(post)! end! ! it 'returns success status' do! expect(response.status).to eq(200)! end! end! end!
  10. Test Reader is not able to see the cause and

    effect between the fixture and the verification logic
  11. require 'spec_helper'! ! describe User do! fixtures :users! ! describe

    '#full_name' do! subject(:user) { users(:superman) }! ! it 'returns combined first name and last name of the user' do! expect(user.full_name).to eq('Clark Kent')! end! end! end!
  12. require 'spec_helper'! ! describe User do! fixtures :users! ! describe

    '#full_name' do! subject(:user) { users(:superman) }! ! it 'returns combined first name and last name of the user' do! expect(user.full_name).to eq('Clark Kent')! end! end! end!
  13. Global When you include fixtures within your tests, They are

    made available to all your tests. ! This means every time you have to add a fixture to deal with an edge case, every other test has to deal with that new data point being part of the test data.
  14. Spread Out Each model gets their own fixture file. If

    that fixture requires a connection to another fixture, you must manage that connection manually.
  15. Brittle guaranteed to break any test that depends on the

    exact result of the model population. ! Example: Reports and Queries
  16. Factories are Local require 'spec_helper'! ! describe User do! describe

    '#full_name' do! subject(:user) do! ! build(:user, ! ! ! first_name: 'Clark', ! ! ! last_name: 'Kent') ! end! ! it 'returns combined first name and last name of the user' do! expect(user.full_name).to eq('Clark Kent')! end! end! end!
  17. FactoryGirl.define do! factory :user do! name { Forgery(:name).full_name }! email

    { Forgery(:internet).email_address }! password { 'aBcdefg1' }! password_confirmation { password }! end! end!
  18. # app/models/user.rb! class User < ActiveRecord::Base! validates :email, uniqueness: true!

    end! # spec/factories/users.rb! FactoryGirl.define do! factory :user do! email '[email protected]'! password 'abcdefg'! end! end!
  19. Questions to Ask Which Environment did they fail on? CI

    or Local? Which Machine? ! What time did they fail at? Time Related? ! Do the Tests depend on any external Resources? Were they Available?
  20. Timecop it 'freezes time' do! time = Time.local(2014, 2, 28,

    10, 0, 0)! Timecop.freeze(time)! sleep(10)! expect(Time.current).to eq(time)! end! ! it 'travels back in time' do! time = Time.local(1955, 11, 5, 10, 0, 0)! Timecop.travel(time)! expect(Time.current).to eq(time)! end!
  21. Prevent Any External HTTP Request # spec/support/webmock.rb! require 'webmock/rspec'! !

    RSpec.configure do |config|! config.before(:each) do! WebMock.reset!! WebMock.disable_net_connect!! end! end!
  22. Use In-Memory Version of Redis require "fakeredis"! ! redis =

    Redis.new! ! >> redis.set "foo", "bar"! => "OK"! ! >> redis.get "foo"! => "bar"!
  23. Mock Calls to Stripe it "mocks a declined card error"

    do! # Prepares an error for the next create charge request! StripeMock.prepare_card_error(:card_declined)! ! expect { Stripe::Charge.create }.to raise_error {|e|! expect(e).to be_a Stripe::CardError! expect(e.http_status).to eq(402)! expect(e.code).to eq('card_declined')! }! end!
  24. Too much detail Feature: Signing In
 Background:! ! Given I

    am not authenticated
 Scenario: Successful Login! ! Given 1 user! ! And I go to the sign in page ! ! When I fill in "Email" with "[email protected]"! ! And I fill in "Password" with "batman"! ! And I press "Sign in"! ! Then I should be on the dashboard path! ! And I should see "Signed in successfully."!
  25. Avoid Implementation Specifics Scenario: Successful Login! ! Given a user

    exists! ! When I sign in! ! Then I should be shown the dashboard!
  26. Use Capybara-RSpec require 'spec_helper'! ! feature 'User sign in' do!

    let!(:user) { create(:user) }! ! scenario 'allows a user to sign in after they have registered' do! sign_in_with(user)! ! expect(page).to have_flash_notice('Signed in successfully.')! end! end!
  27. Write intention Revealing Helpers module Features! module SessionSteps! rspec type:

    :feature! ! def sign_in_form! find('.session form')! end! ! def sign_in_with(user)! ensure_on(new_user_session_path)! within(sign_in_form) do! fill_in('Email', with: user.email)! fill_in('Password', with: user.password)! click_button('Sign In')! end! end! end! end!
  28. Mocks and Stubs are not enough class User < Struct.new(:notifier)!

    def suspend!! notifier.notify("suspended as")! end! end! ! describe User, '#suspend!' do! it 'notifies the console' do! notifier = double("ConsoleNotifier")! ! expect(notifier).to receive(:notify).with("suspended as")! ! user = User.new(notifier)! user.suspend!! end! end! http://rhnh.net/2013/12/10/new-in-rspec-3-verifying-doubles
  29. Using instance_double describe User, '#suspend!' do! it 'notifies the console'

    do! notifier = instance_double("ConsoleNotifier")! ! expect(notifier).to receive(:notify).with("suspended as")! ! user = User.new(notifier)! user.suspend!! end! end!
  30. Available with RSpec Mocks instance_double - Creates a verifying double

    to be used for object instances ! class_double - Creates a verifying double for class methods and modules ! object_double - Create a verifying double from an existing object ! ! !
  31. Great For verifying web request successful? ! verifying correctly redirected

    to the right page? ! verifying a User was successfully authenticated? ! verifying The correct object stored in the response template? ! verifying the appropriate Flash message displayed to the user in the view?
  32. A controller is a 
 class with methods 
 It

    should have tests! http://solnic.eu/2012/02/02/yes-you-should-write-controller-tests.html