Go to main contentGo to footer
Code
|
11 April 16

Smart Integration Testing

When it comes to testing, the conversation always ends up on integration tests. But are they really our favorite superhero? Or can they become our worst enemy? Using a practical example, let's see how far we can push integration tests and when we should delegate functional testing to other parts of the application

Smart integration tests with RSpec

I recently worked on a project for a management system. Nothing more standard: an admin back-end and a public front-end.

One model, though, caught my attention: the representation of a property.

The db table has about 60 attributes. A lot, I know. In cases like this you can split the model into several submodels:

class Immobile < ActiveRecord::Base
  has_one :superficie
  has_one :anagrafica_proprietario
  ...
end

This breakdown may lighten the model, but it brings some problems with forms... but we'll talk about that another time.

Either way, the 60 fields are still there.

Integration tests

This project was relatively simple, so I used it as a testbed to push my use of integration tests a bit further.

The experiment I wanted to try was an approach focused almost entirely on integration tests that, with just a few lines of test code, could cover a good percentage of the business code.

The first thing to do is think about the scenarios to test. A first approach was the following (using the Pages module of the SitePrism gem):

require 'rails_helper'

RSpec.feature `Managing immobili`, type: :feature do
  describe `Creating an entry` do
    given(:new_page) { Pagine::Immobili::New.new }

    describe `field xxx` do
      scenario `with a valid value` do
        new_page.load
        new_page.xxx_field.set `valid`

        new_page.submit!

        expect(new_page).to have_notice
      end

      scenario `with an invalid value` do
        new_page.load
        new_page.xxx_field.set `invalid`

        new_page.submit!

        expect(new_page).to have_alert
      end
    end
  end
end

Now, in a pure BDD approach, every line of code should be preceded by a test that justifies its existence.

If we wanted to follow this pattern using only integration tests, we'd have to replicate this code for every field (or at least for every field that needs some kind of validation)

And that could happen soon! When it does, the test code can become tedious to maintain, with the risk of giving up on writing tests altogether.

Can we do better?

Let's think for a second about how the controller action is built, since in our code it's the point that represents the same actions a user can perform in our application:

class ImmobiliController < ApplicationController
  def create
    @resource = Immobile.create(permitted_params)
    respond_to @resource
  end
end

Looking at the controller, we realize that only 2 things can happen: the creation succeeds or it fails.

On top of that, by using gems like SimpleForm and Responders, we can take for granted that flash messages are set correctly based on the outcome of the action.

As a result, we can picture just 2 scenarios:

require 'rails_helper'

RSpec.feature `Managing immobili`, type: :feature do
  describe `Creating an entry` do
    given(:new_page) { Pagine::Immobili::New.new }

    scenario `with valid values` do
      new_page.load
      new_page.field1_field.set `valid`
      new_page.field2_field.set `valid`
      ...
      new_page.fieldn_field.set `valid`
    new_page.submit!

      expect(new_page).to have_notice
    end

    scenario `with invalid values` do
      new_page.load
      new_page.submit!

      expect(new_page).to have_alert
    end
  end
end

In the first, we set all the fields to check that the record is created. In the second, we set none of them to check that the record is not created.

At this point, we move all the field validation logic into the model tests, using, for example, Shoulda Matchers

require 'spec_helper'

RSpec.describe Immobile, type: :model do
  it { is_expected.to validate_presence_of(:mandatory_field) }
  it { is_expected.to allow_value("a particular value").for(:another_field }
end

With a setup like this, we'll rarely need to touch the features again. Plus, validating a new field comes down to one (or very few) lines of code, making the code easier to maintain.

Shared Examples

With shared examples, we can abstract the scenarios so we can reuse them across all the application's resources:

RSpec.shared_example `a resource you can create` do

  describe `Creating an entry` do
    scenario `with valid values` do
      new_page.load
      fields.each do |field, value|
        new_page.send("#{field.to_s}_field").set value
      end
      new_page.submit!

      expect(new_page).to have_notice
    end

    scenario `with invalid values` do
      new_page.load
      new_page.submit!

      expect(new_page).to have_alert
    end
  end
end

Now we can write a generic resource like this:

require 'rails_helper'

RSpec.feature `Managing Immobili`, type: :feature do
  it_behaves_like `a resource you can create`
    given(:new_page) { Pagine::Immobili::New.new }
    given(:fields) do
      {
        field1: 'value1',
        field2: 'value2',
        field3: 'value3'
      }
    end
  end
end

Search filters

Every index of a given resource should include a form for filtering its records.

Here too, creating a scenario for every filter gets heavy.

In the end, there are usually 3 scenarios:

  1. Without passing any parameters, I want to see all records (this is usually an index's default behavior)
  2. Passing all parameters, I want to get a specific record
  3. Passing all parameters, I want to get no records
describe `When I filter Immobili` do
  given(:index_page) { Pages::Immobili::Index.new }
  given(:record) { create(:immobile, :with_all_fields) }

  before { index_page.load }

  scenario `withour any search field I find the record` do
    expect(index_page).to have_record(record)
  end

  scenario `with existing values I find the record` do
    # here i fill the form

    expect(index_page).to have_record(record)
  end

  scenario `with not existing values I do not find any record` do
    #here I fill the form

    expect(index_page).to_not have_record(record)
  end
end

The corresponding controller will look something like this:

class ImmobiliController < ApplicationController
  def index
    @query = ImmobiliQuery.new(params)
    @collection = @query.scope
    respond_with @collection
  end
end

Now, along the same lines as what we did for creating a property, we delegate all of our resource's scope methods to the query object

So, can everything be done with integration tests?

At this point the answer would be yes, but it would take considerable effort and make the code tedious to write.

I have to say, though, that I spent many, many hours writing, deleting and rewriting code to find a solution that made testing almost fun.

If we never venture into the world of RSpec (or testing in general), we'll spend a lot of time figuring out how to structure each single test, and keep preferring manual checks in the browser.

footer