Back in January, Sandi Metz introduced her rules for developers in a Ruby Rogues podcast episode episode. Around the time Sandi’s rules were published, the team I am on was starting a new project. This post details the experience of that team applying Sandi’s rules to the new application.
The…
Tim Ferris on how to learn anything
Your tests are slow? Put them in solitary confinement
As Rails matures and starts appearing in more and more organisations, I’m starting to notice a trend amongst the companies I work with. Maybe this might sound familiar…
They have an app that’s a few years old, it was an MVP that became a product. Originally it was built by 1 or 2 people but now it’s maintained by a larger team. Through perseverance and determination, the code is, in general, in a decent state, no-one’s saying that dreaded R-word (refactor).
Test coverage is also looking pretty good, it provides the developers with confidence no matter what part of the app they are working on.
This is all sounding pretty good, so what’s the problem?
Firstly, by now the app is a giant ball of code. What was a simple MVP, grew in complexity along with it’s success to become a feature laden monolithic beast.
Secondly, have you tried running the full test suite? It’s slow and not just in an inconvenient, ‘I have to wait a minute’ for the suite to run. But in a ‘I’ll go for lunch while the tests run and I’ll still be waiting when I get back’ way.
So, what should you do about it?
Well, I’ll deal with the second problem first, because a faster test suite will improve your life as a developer so much.
Isolate
One of the keys to fast tests is to only test what you need to test. If you don’t need to test a database interaction, then don’t. I see so many tests where the setup involves creating lots of objects in the database, purely because it’s ‘easier’ than isolating the test properly.
Stubs and mocks are your friends on this journey to a faster test suite. The great thing about this isolation approach is that it quickly reveals areas that are coupled together, usually if a test requires a lot of setup this is a good indication of coupling, which gives you a good place to begin improving your code.
There will be cases where you have to interact with the database, testing scopes or custom finder methods is a common example. In this context, hitting the db in these tests is fine. The important thing to remember is to keep a clear focus on what you are testing and isolate it from everything else.
Next Level Isolation
Ok, so now you’ve got all your tests in isolation, no stray objects interfering with your object under test. Can we go faster?
Yes… it’s time for the ultimate in isolation. If your framework, eg. Rails, is not the subject under test then you should be isolating your tests from it.
There’s been some good stuff written about this already. See the talks by Gary Bernhardt and Corey Haines.
Basically, most of our tests don’t depend on our framework (in this case Rails), so we should treat it like any other dependency.
So, imagine we’re building a Cart and part of it’s responsibility is to calculate the total value of every line item it contains. This has nothing to do with Rails, it’s purely a case of performing a calculation on a collection of objects. So let’s test it like that.
How about some code…
# spec/spec_faster_helper.rb
$: << File.expand_path('../../app/models', __FILE__)
# Stub out ActiveRecord
module ActiveRecord
class Base; end
end
# spec/models/cart_spec.rb
require 'spec_faster_helper'
require 'cart'
describe Cart do
let(:cart) { Cart.new }
describe '#total_value' do
context 'with 2 line items' do
let(:line_item) { double 'line_item', price: 1 }
before { cart.stub(line_items: [line_item, line_item] }
it 'calculates the total price of all the line items' do
expect(cart.total_value).to eq 2
end
end
end
end
# app/models/cart.rb
class Cart < ActiveRecord::Base
has_many :line_items
def total_value
line_items.map(&:price).inject(&:+)
end
end
Regardless of the speed benefits, this is good because it’s isolating the class under test from another dependency, Rails. The bonus is that this spec will run faster than it’s counterpart written using the default spec_helper provided by rspec-rails.
Summary
Hopefully this will help provide an actionable intro into speeding up your tests and making your development life a little easier.
Animate your Photoshop iOS prototypes. Killer idea.
Interesting approach to development.
Interesting take on a trip to San Francisco.
Looks like a really interesting addition to the increasingly crowded space of wysiwyg web design tools.
Ruby gotcha: Enumerable .all?
I recently came across something unexpected when working with an Array in Ruby. When using .all? and the Array (or Hash) is empty, it always returns true.
Some code…
[].all? #=> true
[].all?(:exists?) #=> true
{}.all? #=> true
I was using it to check the value of an association, something a bit like this…
class Checkout
has_many :orders
def orders_are_valid?
orders.all?(&:valid?)
end
end
checkout = Checkout.new
checkout.orders_are_valid? #=> true
I don’t know about you, but this return value took me by surprise. I was expecting an empty array to return false. Either way, it’s good to be aware of this.
The public beta of HubSpot’s Culture Code slide deck. Take an inside look at HubSpot, the people behind it, and what makes us tick.
Run single and multiple lines in RSpec
Sometimes you need to run a single line from your spec. Use this handy shortcut…
rspec path/to/a_spec.rb:12
What if the spec lacks suitable describe or context blocks for you to run a group of specs? Simple, you can pass multiple line numbers…
rspec path/to/a_spec.rb --line_number 37 --line_number 42
rspec path/to/a_spec.rb:37:42