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

4 Steps to Faster Rails Tests

4 Steps to Faster Rails Tests

This talk was presented at Magrails on October 14th 2011. Check magrails.com for the video, and github.com/seenmyfate for sample code

Tom Clements

October 14, 2011
Tweet

More Decks by Tom Clements

Other Decks in Programming

Transcript

  1. 4 Steps to Faster Rails Test
    Tom Clements
    Senior Developer, On The Beach
    tom-clements.com | github.com/seenmyfate
    @Seenmyfate

    View full-size slide

  2. A question
    How long does it take to run your unit tests?

    View full-size slide

  3. Let's look at some numbers
    $ time rake spec
    31.486 seconds
    $ time bundle exec rspec spec
    15.741 seconds
    $ time rspec spec
    14.932 seconds

    View full-size slide

  4. And what am I testing?

    View full-size slide

  5. Nothing!
    # spec/nowt_spec.rb
    require 'spec_helper'
    describe "Nothing" do
    # nada
    end
    0 examples, 0 failures
    15.741 seconds total

    View full-size slide

  6. So how can we make this
    better?

    View full-size slide

  7. Here's some common suggestions
    Don't Test
    Get an SSD
    Use Spork

    View full-size slide

  8. these solutions are just
    masking the problem
    And the first part of that problem is
    spec_helper

    View full-size slide

  9. spec helper is the best way to write really slow painful specs
    Gary Bernhardt

    View full-size slide

  10. With spec_helper - 15.741 seconds
    Without:

    View full-size slide

  11. 0.186 seconds
    # spec/some_spec.rb
    # require 'spec_helper'
    describe "Nothing" do
    # nada
    end
    0 examples, 0 failures
    0.186 seconds total

    View full-size slide

  12. So here's the question

    View full-size slide

  13. For our unit tests -
    Why are we loading the entire rails
    environment?
    Because our business logic is in our active
    record models
    And we need rails to run tests against
    classes that inherit from
    ActiveRecord::Base
    Because we're breaking SRP

    View full-size slide

  14. Single Responsibility
    Principle
    A class should only have one reason to
    change

    View full-size slide

  15. 4 Steps
    Extract business logic into modules
    Extract domain objects into classes
    Mixin and delegate
    Test in isolation

    View full-size slide

  16. An example
    class Basket < ActiveRecord::Base
    has_many :basket_items
    def total_discount
    basket_items.collect(&:discount).sum
    end
    end

    View full-size slide

  17. An example
    require 'spec_helper'
    describe Basket do
    context "total_discount" do
    let(:basket) { Basket.create! }
    let(:basket_items) { [BasketItem.create!(:discount => 10),
    BasketItem.create!(:discount => 20) ]}
    it "should return the total discount" do
    basket = Basket.create!
    basket.basket_items = basket_items
    basket.total_discount.should == 30
    end
    end
    end

    View full-size slide

  18. 7.092 seconds to run
    time rspec spec
    .
    Finished in 0.24435 seconds
    1 example, 0 failures
    rspec spec 7.092 total

    View full-size slide

  19. Extract behaviour into
    modules
    module DiscountCalculator
    def total_discount
    basket_items.collect(&:discount).
    inject(:+)
    end
    end

    View full-size slide

  20. Mixin
    class Basket < ActiveRecord::Base
    has_many :basket_items
    include DiscountCalculator
    end

    View full-size slide

  21. And the test
    require 'discount_calculator'
    class FakeBasket
    include DiscountCalculator
    end
    describe DiscountCalculator do
    context "#total_discount" do
    it "should return the total discount" do
    basket = FakeBasket.new
    basket_items = [stub(:discount => 10),
    stub(:discount => 20)]
    basket.stub(:basket_items) { basket_items }
    basket.total_discount.should eq 30
    end
    end
    end

    View full-size slide

  22. 0.350 seconds to run
    time rspec spec
    .
    Finished in 0.00121 seconds
    1 example, 0 failures
    rspec spec 0.350 total

    View full-size slide

  23. Extract domain objects into
    classes
    class DiscountCalculator
    def total_discount(items)
    items.collect(&:discount).inject(:+)
    end
    end

    View full-size slide

  24. And delegate
    class Basket < ActiveRecord::Base
    has_many :basket_items
    def total_discount
    DiscountCalculator.new.
    total_discount(basket_items)
    end
    end

    View full-size slide

  25. And the test
    require 'discount_calculator'
    describe DiscountCalculator do
    context "#total_discount" do
    let(:items) { [stub(:discount => 10),
    stub(:discount => 20)] }
    it "should return the total discount" do
    calculator = DiscountCalculator.new
    calculator.total_discount(items).should eq 30
    end
    end
    end

    View full-size slide

  26. 0.342 seconds to run
    time rspec spec
    .
    Finished in 0.00101 seconds
    1 example, 0 failures
    rspec spec 0.342 total

    View full-size slide

  27. The benefits
    Higher Cohesion
    Lightning fast tests
    Happier developing
    Try it out
    github.com/seenmyfate

    View full-size slide

  28. Further viewing/reading
    @coreyhaines - confreaks.net/videos/641-gogaruco2011-fast-rails-
    tests
    @garybernhardt - destroyallsoftware.com
    @martinfowler - objectmentor.com/resources/articles/srp.pdf

    View full-size slide

  29. 4 Steps to Faster Rails Test
    Tom Clements
    Senior Developer, On The Beach
    tom-clements.com | github.com/seenmyfate
    @Seenmyfate

    View full-size slide