The Problem

You often want something to happen at a stage in an ActiveRecord object’s standard workflow - most often this is “After I save I want to do X.”

For Example:

class File < ActiveRecord::Base
  validates :file_path, :presence => true
end

File.save # => Persist, if successful,
# create a thumbnail version for the web.

Why Callbacks

An often recommended way to deal with this is as an after_save callback.

class File < ActiveRecord::Base
  validates :file_path, :presence => true
  after_save :create_thumbnail
  
  private

  def create_thumbnail
    ThumbnailCreator.call(file_path)
  end
end

PROs:

  • Simple, concise, all the logic is in one place you can look at.
  • You can never accidentally save a file without it creating a thumbnail.

Before I get to the CONs, let’s look at some assumptions made:

Assumptions:

  • We’re okay opening up the file class whenever we want to change when and how we create a thumbnail.
  • We ALWAYS want to create a thumbnail.
  • If we save a file, we’re okay with whatever overhead exists to create that thumbnail.
  • We’re okay with not injecting dependencies (ThumbnailCreator), and thus when that changes we’re happy to change it in File.

CONs:

  • State is mixed up with business logic. Suddenly my “File” is not just “attributes with persistence”, and is now “attributes with persistence and side effects too”
  • NOT creating a thumbnail is hard. How do I avoid callbacks? Only this SPECIFIC callback?
  • Doing something like pushing that thumbnail creation to the background is complicated logic that will sit in the model.
  • ThumbnailCreator is hard-wired, its responsibility is inflexible.

Why Not Callbacks

It turns out the assumption that we ALWAYS want to create a thumbnail is a big one. It’s rare that you ALWAYS want something to happen, and building in that assumption is the beginning to expensive backtracking or hacks later.

The big point is this: business logic changes at a different rate than object representation. Tying the logic of when and how to create a thumbnail to the representation of how a File is stored means coupling changes in the File to changes in how a thumbnail is created, unnecessarily.

Alternatives

Service Objects

We can encapsulate what to do when an object is saved by putting all of it in one object.

class FileSaver
  attr_reader :file, :thumbnail_creator
  def initialize(file, thumbnail_creator)
    @file = file
    @thumbnail_creator = thumbnail_creator
  end

  def run
    if file.save
      thumbnail_creator.call(file.file_path)
    end
  end
end

This works - you can inject ThumbnailCreator, you only use the FileSaver when you want, and you will never accidentally create a thumbnail.

However, what tends to happen is that ALL behavior gets stored here and the problem of “how do I ignore just ONE callback action” remains.

Decoration of Functionality

We can decorate JUST this functionality onto the object.

class CreatesThumbnails < SimpleDelegator
  attr_reader :thumbnail_creator

  def initialize(file, thumbnail_creator)
    @thumbnail_creator = thumbnail_creator
    super(file)
  end

  def save
    if __getobj__.save
      thumbnail_creator.call(file_path)
    end
  end
end

CreatesThumbnails.new(File.new(:file_path => "/file"), ThumbnailCreator).save

Now when you want to create a thumbnail on save you can decorate the object with this, call save, and you’re good. If when or how to run the thumbnail creator changes, you can inject a new thumbnail creator or change just the CreatesThumbnails object. Business logic is now separated from state.

Decorating all the necessary actions and dependencies for a transaction can get complicated, at which point the Factory Pattern can help. I may do a post on this at a later date.


To see the previous post in this series please check out How I Used to Test

What Now?

Integration tests with Capybara had failed me and in my experience there was only one other option: Unit Tests.

My reasoning went like this: People had been automatically testing applications, even web applications, long before they could automatically launch a browser and click around with a clean DSL. How did they do it?

The answer is messages.

Trust Your Interface

Rather than testing that all the pieces hook together, you test that each of the parts (units) send the right messages to the right collaborators, and that those collaborators do what you expect them to do. The web, and Ruby on Rails, has a fairly specific workflow that you can trust. It goes like this:

URL In Bar -> Route -> Controller -> View

Fortunately, RSpec has tests for each of these layers.

Routing Tests

Testing that a route goes to the right place looks like this:

# spec/routing/post_routing_spec.rb
require 'rails_helper'

RSpec.describe "post routes" do
  it "should route /posts/1 to the posts controller" do
    expect(get "/posts/1").to route_to :controller => :posts, :action => :show, :id => "1"
  end
end

If you run this and the controller doesn’t exist, it will tell you. However, it won’t tell you if the action doesn’t exist.

Controller Tests

Now (assuming the above test passes), you have a route that goes to a specific controller, an action, and sets a parameter. Time to write the controller spec, to make sure it behaves appropriately.

# spec/controllers/posts_controller_spec.rb
require 'rails_helper'

RSpec.describe PostsController do
  describe "show" do
    before do
      @post = Post.create
      get :show, :id => @post.id.to_s
    end
    # Ensures that @post is set for the view to use
    it "should set @post" do
      expect(assigns(:post)).to eq @post
    end
    it "should render the show template" do
      expect(response).to render_template "posts/show"
    end
  end
end

This test will complain if a route doesn’t exist, if the action doesn’t exist, and will properly fail if either the instance variable or the template isn’t set.

View Specs

Now we know the controller responds to the right route, populates an instance variable, and renders the appropriate template. Now how do we prove that the template shows the right information?

Enter view specs.

# spec/views/posts/show.html.erb_spec.rb
require 'rails_helper'

RSpec.describe "posts/show.html.erb" do
  let(:post) { Post.create(:title => "Test") }
  before do
    assign(:post, post)
    render
  end
  it "should show the post's title" do
    # This requires Capybara to be installed,
    # since it's using its matchers.
    expect(rendered).to have_content "Test" 
  end
end

What’s Wrong With This?

This leaves just a little bit of a hole - nothing right now actually enforces that these connections exist. At each layer you’re trusting that the other layer has tests to ensure that it’s doing the right thing. There’s likely room for some automated contract verification, but it doesn’t exist yet. A simple integration test which just fails if the pieces fall down is probably in order.

# spec/features/show_posts_spec.rb
require 'rails_helper'

feature "posts" do
  background do
    @post = Post.create(:title => "Test")
  end
  scenario "visiting a post page" do
    expect{visit posts_path(@post)}.not_to raise_error
    # May or may not be required depending on your Rails settings.
    expect(page).not_to have_content "Exception"
  end
end

Wait! We just wrote an integration test. Weren’t we avoiding that? Let’s go over why we avoided integration tests in the first place:

The Goals

Tests that are:

  1. Fast
  2. Predictable
  3. Informative
  4. Reliable

There is only one feature spec - and there will always be only one feature spec for a given feature. Reducing this number to 1 means that tests will be fast and there’s very little chance the browser will get in the way.

Now we’re left with:

  1. Fast
  2. Predictable
  3. Informative
  4. Reliable

Informative

Test Driven Development is supposed to drive good design of an application. We have a fast test suite now, but at no point did the tests actually warn us that we may be doing something bad. For instance, the PostsController could look like this:

class PostsController
   def show
     @post = post_factory.new
   end

   private

   def post_finder
     PostFinder
   end

   def post_factory
     PostFactoryFactory.new(post_finder.new(self, params, :with_cached))
   end
end

The tests wouldn’t complain, so long as those classes exist. The test would be easy to write, and just as concise.

The reason is this: the complexity in programming comes from branches and dependencies. In these tests we’ve hidden away that dependencies exist - if tests are written such that they tell us about them then the complexity becomes apparent while writing the test.

Integrated Tests Are A Scam - Great talk by J.B. Rainsberger about why integrated tests eventually calcify and his proposal on what to do about it.

Up Next

Next Post in Series: Isolated Testing


Starting Out

I’ll always remember my first interview for a real development position. It was for a developer position here at Oregon State University as a PHP programmer. I was asked to report on a project I had completed in the past and then asked a series of questions about how I develop. The one that stands out the most went like so:

Interviewer: How do you test that your applications work?

There was only one answer, obviously.

Me: Well, first I open the app in a browser and I...

You can see where I was going. It wasn’t the answer they were looking for and I could feel it. Time to figure out what went wrong.

Dipping The Toes

I got my first full time job shortly afterwards, having answered a similar question by saying “well, I’m familiar with Test Driven Development, but I’ve never done it.”

Fortunately they were nice.

I knew I needed to figure out how to test, and they had me learning Ruby, so it was time to explore the options:

Potential Tools

  1. Test::Unit
  2. RSpec
  3. Capybara
  4. Cucumber

Alright, lots of people are using RSpec, I quickly decided I didn’t like Cucumber, and then…there was Capybara.

Capybara

Capybara is a testing framework in ruby which tests web applications by simulating a browser. It goes to pages, clicks through forms, and validates that text is on the page. An automated Capybara test (back then) looked like this:

require 'spec_helper'

describe "viewing posts" do
  before do
    @post = Post.create(:title => "My Title")
    visit "/posts"
  end
  it "should display all the post titles" do
    expect(page).to have_content("My Title")
  end
end

Finally something which automated everything I would normally do by hand! Things were taking seconds, rather than minutes, to make sure they work. If I wrote a whole suite of these Integration tests, then I could be sure the site was working and could refactor my code at will.

Some Problems

A few little problems popped up.

  1. Testing Was Slow

    Testing my application was taking something like 30-60 minutes to run through the full set of tests.

    Oh well, I could work around that - throw in a Rails preloader like spring, run only the specs I care about while developing, and then things only hurt when I had to make sure the all the tests were done. Solved

  2. Test Driven Development

    I kept reading that I should be writing tests first, and it was hard. Often times the last thing I knew was what the UI would look like - how could I write a test that included information about it? Oh well, do my best. Not So Solved

  3. Code Design

    This felt like the biggest problem to me - I kept hearing that tests should make me recognize when my code needed help, but none of my tests were saying anything about the code - just the application that it created. I could refactor without changing tests, yes, but I could also NOT and it would all feel the same. Not Solved

  4. Unreliable

    Sometimes I’d run my tests and they’d fail - but seemingly randomly. I couldn’t trust the application unless they passed, but there was no way to find out why they were failing (often times it’d be a race condition within capybara or my application logic that couldn’t be reproduced by a user), so all I could do was restart the tests and wait for them to pass - another 30-60 minutes down the drain.

    Maybe this was a problem with my tests, or my system legitimately failed sometimes. The issue was there was no way to tell: every test ran through so much that I couldn’t find what was going on, and had to attribute it to bugs in the workflow of spinning up a browser.

Now What?

I knew I had some problems, and I knew if I wanted stable, reliable, functional applications that I’d have to solve them. I needed automated tests which were:

  1. Fast
  2. Predictable
  3. Informative
  4. Reliable

Integration tests had failed me, there was only one other option - Unit Tests

My next post will be all about the resources I used to get where I am now, and what I think the solution to the problems above are.

Next Post in Series: Avoiding Integration Tests