RSS

CEK.io

Chris EK, on life as a continually learning software engineer.

Testing, Low- to High-Level

Google “low level” and you’ll see the following definition.

We often hear about low-level languages like Assembly or C (which was once a high-level language) vs. high-level languages like Java or Ruby (which may come to be considered low-level?).

Applying that same logic to testing, this blog post covers the various types of tests for Rails applications, working its way up from low-level unit tests up to high-level acceptance tests, with functional and integration tests in between. Along the way, I’ll reference examples of tests I wrote for a side project: my World Cup tracker.

Unit Tests (Model Tests)

The Rails Guide on testing describes unit tests as “what you write to test your models.” It continues, “It’s a good practice to have at least one test for each of your validations and at least one test for every method in your model.”

I won’t get into FactoryGirl in detail, but here I create a factory for the Team model, which I use instead of fixtures as test data for team_spec.rb.

(teams_factory.rb) download
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
FactoryGirl.define do
  factory :team do
    api_id "A0CD1355-B6FC-48D3-B67B-AF5AA2B2C1E1"
    name "Croatia"
    logo "http://cache.images.globalsportsmedia.com/soccer/teams/150x150/514.png"
    website "http://www.hns-cff.hr/"
    group_letter "A"
    group_rank 3
    group_points 3
    matches_played 3
    wins 1
    losses 2
    draws 0
    goals_for 6
    goals_against 6
    goal_differential "+0"
  end
end
(team_spec.rb) download
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
require 'spec_helper'

describe Team do
  it 'has a valid factory' do
    expect(build(:team)).to be_valid
  end

  let(:croatia) { FactoryGirl.create(:team) }
  let(:brazil) { FactoryGirl.create(:team, :name => "Brazil", :group_rank => 1, :group_points => 7, :wins => 2, :losses => 0, :draws => 1, :goals_for => 7, :goals_against => 2) }

  describe '#next_match' do
    context 'when there are upcoming matches' do
      it 'returns the match object' do
        pending("pending until test can retrieve match objects")
        this_should_not_get_executed
      end
    end
  end

  describe '#next_opponent' do
    context 'when there are upcoming matches' do
      it 'returns the team object' do
        pending("pending until test can retrieve team objects")
        this_should_not_get_executed
      end
    end
  end

  describe '#avg_goals_against' do
    it 'returns the avg goals against value' do
      expect(croatia.avg_goals_against).to eq(2.0)
    end
  end

  describe '#avg_goals_for' do
    it 'returns the avg goals for value' do
      expect(brazil.avg_goals_for).to eq(2.33)
    end
  end
end

Functional Tests (Controller Tests)

The testing RailsGuide explains, “In Rails, testing the various actions of a single controller is called writing functional tests for that controller. Controllers handle the incoming web requests to your application and eventually respond with a rendered view.” RSpec’s documentation explains “A controller spec is an RSpec wrapper for a Rails functional test.” So functional tests and controller tests are synonymous.

The main purpose of functional tests, as the quote above makes clear, is to ensure that individual controllers handle requests appropriately and render the proper view. This can mean testing the following:

  • Was the web request successful?
  • Was the user redirected to the right page?
  • Was the user successfully authenticated?
  • Was the correct object stored in the response template?
  • Was the appropriate message displayed to the user in the view?

In the following example, I simply test that TeamsController assigns the correct resources (teams) and renders the correct views on get requests.

(teams_controller_spec.rb) download
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
require 'rails_helper'

describe TeamsController do
  describe 'GET index' do
    it 'assigns @teams' do
      team = Team.create
      get :index
      expect(assigns(:teams)).to eq([team])
    end

    it 'renders the index template' do
      get :index
      expect(response).to render_template('index')
    end
  end

  describe 'GET show' do
    it 'renders the show template' do
      team = Team.create
      get(:show, {'id' => "1"})
      expect(response).to render_template('show')
    end
  end

  # ...
end

Integration Tests (Acceptance Tests)

Depending who you ask, integration tests and acceptance tests are either identical or subtly different. They both test functionality from end-to-end (or close to it) and are both forms of black box testing, but acceptance tests have a customer orientation, whereas integration tests are written by developers for developers. The first five slides of this talk provide a helpful summary.

The testing RailsGuide defines integration tests as intended to test “the interaction among any number of controllers. They are generally used to test important work flows within your application.” The “important work flows” are the key; integration tests should test the application from end-to-end—across multiple models, controllers, and views—to ensure that the application works “in integration”.

ExtremeProgramming.org defines acceptance tests as follows: “Acceptance tests are created from user stories. During an iteration the user stories selected during the iteration planning meeting will be translated into acceptance tests. … Acceptance tests are black box system tests. Each acceptance test represents some expected result from the system.” Another key: black box testing, testing an for the proper output given a certain input, without knowing what happens inside the “black box” (that’s what unit and functional tests are for).

(team_index_spec.rb) download
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
require 'spec_helper'

describe 'Team index page' do

  before(:each) do
    FactoryGirl.create(:team, :name => "Croatia")
  end

  it 'renders the correct text' do
    visit '/teams'
    expect(page).to have_text('2014 World Cup Teams')
    expect(page).to have_text('Croatia')
  end

  # ...
end

Conclusion

CodeClimate describes what they call the Rails Testing Pyramid, depicted below:

They write, “Blending unit tests, service-level tests and acceptance tests yields faster test suites that still provide confidence the application is working, and are resistant to incidental breakage. As you develop, take care to prune tests that are not pulling their weight. When you fix a bug, implement your regression test at the lowest possible level. Over time, keep an eye on the ratio between the counts of each type of test, as well as the time of your acceptance test suite.”

In the case of the Team resource in my World Cup app, my ratio was 1 integration test:3 functional tests:5 unit tests—a perfectly pyramidal ratio if you ask me! Of course, this is a simple example, but it illustrates the goal for a test suite more generally.

Resources