Skip to content

sf-wdi-27-28/testing-with-rspec

 
 

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

3 Commits
 
 

Repository files navigation

Testing with RSpec

Objectives
Understand the difference between unit and integration tests.
Identify various aspects of Rails apps that we might want to test.
Test model methods using rspec-rails.
Test controller actions using rspec-rails.

Resources

Resource Description
RSpec matchers Reference for RSpec
shoulda Magic for model specs
FactoryGirl Factories let you build up objects quickly for your specs
DatabaseCleaner Cleans out your database before each test.

Unit tests

Tests can be broadly split into two categories. Unit Tests and Integration Tests. Both are important.

In Unit Tests which we'll talk about today we try to isolate each component (or class/method) and test it on it's own. We separate our Controllers from our Views and test the boundary of its interface.
Unit tests tend to run faster because we're testing small components. By having to isolate components from each other to test them we're forced to write better OO code. The functionality can't blur across several modules without us having to do a lot of work in the test to stub that out.

In Integration Tests we combine components together, sometimes just a few and other times the entire system. In Rails, integration tests often drive a browser and test the entirety of the system--the full request response cycle. These tests tend to take much longer to run. They test the collusion of components and that the interface between them is behaving as we expect.

Both types of tests are important. There are also other types but they can generally be broken down into finer grained versions of the above. Together the Unit and Integration tests you write become part of your test suite.

How are tests used in industry?

Many companies require that all the code they develop comes with tests. Often before merging code into master, the entire test suite is run and all tests must pass.

rspec-rails

RSpec is a testing gem for Ruby. It helps us write tests that sound like user stories or planning comments ("This method does..."). <a href"https://github.com/rspec/rspec-rails" target="_blank">rspec-rails is a testing framework specifically for Rails. We'll use rspec-rails to test our models and controllers.

rspec-rails helps us implement the four-phase testing methodology (with setup, exercise, verify, and tear down steps). Here's what a simple rspec-rails test might look like:

#
# spec/models/pet_spec.rb
#

RSpec.describe Pet, type: :model do

  # setup
  let(:pet) { Pet.create({name: "Olive", age: 4}) }

  describe "#is_cute?" do
    it "returns true" do
      expect(pet.is_cute?).to be true   #exercise and verify   
    end
  end

  # teardown is automatic

end

Adding rspec-rails to Your Project

  1. Add rspec-rails to your Gemfile in the development and test groups:
#
# Gemfile
#
 group :development, :test do
   gem 'rspec-rails'
 end
  1. Run bundle install (or bundle for short) in your terminal so that rspec-rails is actually added to your project.

  2. Add tests to your rails project using the terminal:

$ rails g rspec:install

This creates a spec directory. It also adds spec/spec_helper.rb and .rspec files that are used for configuration. See those files for more information.

  1. Configure your specs by going into the .rspec file and removing the line that says --warnings if there is one. Consider adding --format documentation

  2. If you created models before adding rspec-rails, create a spec file for each of your models. (This is only necessary if you had a model created before you installed rspec-rails.)

$ rails g rspec:model MODEL_NAME

Running RSpec-rails Tests

Typical spec folders and files for a Rails project include:

  • spec/models/user_spec.rb
  • spec/controllers/users_controller_spec.rb
  • spec/views/user/show.html.erb_spec.rb
  • spec/features/signup_spec.rb

As you can see spec files should always be named ending in _spec.rb.

To run all test specs, go to the terminal and type rspec or bundle exec rspec.

To run only a specific set of tests, type rspec and the file path for the tests you want to run in the terminal:

# run only model specs
rspec spec/models

# run only specs for `ArticlesController`
rspec spec/controllers/articles_controller_spec.rb

# To search for and run a single spec inside a file:
rspec spec/controllers/articles_controller_spec.rb -e 'is cute'

Run rspec from the terminal now to check that your install worked.

Writing rspec-rails Tests

Anatomy of a test

A test should consist of:

  1. Setup: Using let or before or subject to preconfigure data that is needed to test or set the test subject. You can keep your code dry by re-using these across multiple tests.

    • Including Definition: A name for the test. This should use an active verb. Ex. "is invalid without an email". This should also be descriptive enough that it can be used as documentation by other developers. This isn't strictly one of the 4 parts of a test, but it IS really important, future developers will like you if your test name tells them what the code should do.
  2. Exercise: Any code inside the test-block itself that makes a change to the object under test prior to validating that it behaves properly.

  3. Validation: Finally validating that the Object Under Test has behaved in the expected way. In RSpec this usually involves using expect. Sometimes this is called the assertion.

  4. Tear-down: Cleaning up after the test. Usually this is handled for you by RSpec and may include using DatabaseCleaner to wipe the testing database.

Setup

  subject(:cat) { Animal.new(type: 'cat', name: 'fluffy') }
  let(:food) { Food.new }

  before do
    food.flavor = 'chicken gizzards'
  end
  • Use subject to define the item being tested.
  • Use let to set variables for a test.
    • These are reset when each test starts!
  • Use before to set more complex pre-test steps.
    • before blocks can use variables defined in let
  • let!, subject, before and after blocks are all run for each test. Values in them are reset for each test.

Definition & Exercise

  describe '#eat' do
    it "isn't hungry after eating" do
      cat.eat(food)
  • Use active verbs for test names.
  • use 'it is valid' rather than 'it should be valid'
  • it is for individual tests, divide the tests up using describe and context
  • Exercise can be any extra logic that needs to be run to combine the object under test with its collaborators, or to run the method you are testing.
  • Sometimes exercise and validation are on the same line; that's ok.

Validation

This is where we make assertions about the object under test and its behavior.

   expect(cat.hungry?).to be false
  • In general test one and only one thing per test.
    • However that can sometimes mean using more than one expect.

Tear down

Usually RSpec and other gems we might be using take care of most of this for us. However in some cases you may need to do some sort of cleanup.

after do
  cat.pet
end

What do we test?

  • isolation
  • behavior
  • by component

We try to test each component or piece independently. Code written following good object-oriented practices and with concerns well separated is far easier to test. So, if we write our tests before our code our tests can help to push us to write good object-oriented code and to separate concerns.
Break tests into test files for each class. And then groups of tests for each method in the class. And then possibly into contexts for specific conditions under which the method may be used. (E.g. with valid or invalid data, with strings or integers, when x=true or x=false).

Isolate tests from each other. One test should never depend on another test to change or prepare something. Each test should be able to run on its own without the others.

Test behavior.

Testing Models

FactoryGirl

We can set up a User instance for testing purposes with User.create or we can use a tool called FactoryGirl to do this for us.

#
# spec/models/user_spec.rb
#
require 'rails_helper'
RSpec.describe User, type: :model do

  subject(:user) { FactoryGirl.create(:user) }

end

#
# spec/factories/user.rb
#
FactoryGirl.define do
  factory :user do
    sequence(:email) { |n| "g#{n}@g.com" }
    password "testtest"
    first_name 'Jon'
    last_name 'Snow'
    confirmed_at { Time.now }
  end
end

It's also possible to use FFaker to generate some data either for User.create or for FactoryGirl. But FFaker can run into intermittent issues because it can produce duplicate data or results you may not expect. Therefore many developers prefer to use FactoryGirl's sequence.

Assuming we've already set user with first and last names, we can then test that the full_name method correctly calculates the full name:

#
# spec/models/user_spec.rb
#
require 'rails_helper'
RSpec.describe User, type: :model do

  ...

  describe "#full_name" do
    it "joins first name and last name" do
      expect(user.full_name).to eq("#{user.first_name} #{user.last_name}")
    end
  end

end

shoulda

Previously we talked about model validations. You'll probably want to test these. The shoulda gem provides an easier way to write specs for common model validations.

Validating that a Post is invalid without a title:

    it "is invalid without a title" do
      post = Post.new(description: 'foo')
      expect(post.valid?).to be false
    end
  end

The same test as above written using shoulda:

it { should validate_presence_of(:title) }
  • shoulda also provides test helpers for controllers
  • See the shoulda docs

Testing Controllers

To test authentication, we need to define a current_user before each of our tests run. The last line in this before do block -- allow_any_instance_of(... -- creates a "stub" (fake) current_user instance method for the ApplicationController and sets it up as a getter that only ever returns the @current_user we made with ffaker.

#
# spec/controllers/articles_controller_spec.rb
#
require 'rails_helper'
RSpec.describe ArticlesController, type: :controller do

  let(:signed_in_user) { FactoryGirl.create(:user) }

  before do
    # stub a method on ApplicationController
    allow_any_instance_of(ApplicationController).to receive(:current_user).and_return(signed_in_user)
  end

  describe "GET #index" do
    it "assigns @articles" do
      all_articles = Article.all
      get :index
      expect(assigns(:articles)).to eq(all_articles)
    end

    it "renders the :index view" do
      get :index
      expect(response).to render_template(:index)
    end
  end

  describe "GET #new" do
    it "assigns @article" do
      get :new
      expect(assigns(:article)).to be_instance_of(Article)
    end

    it "renders the :new view" do
      get :new
      expect(response).to render_template(:new)
    end
  end

  describe "POST #create" do
    context "success" do
      it "adds new article to current_user" do
        articles_count = signed_in_user.articles.count
        post :create, article: {title: "blah", content: "blah"}
        expect(signed_in_user.articles.count).to eq(articles_count + 1)
      end

      it "redirects to 'article_path' after successful create" do
        post :create, article: {title: "blah", content: "blah"}
        expect(response.status).to be(302)
        expect(response.location).to match(/\/articles\/\d+/)
      end
    end

    context "failure" do
      it "redirects to 'new_article_path' when create fails" do
        # create blank article (assumes validations are set up in article model for presence of title and content)
        post :create, article: { title: nil, content: nil}
        expect(response).to redirect_to(new_article_path)
      end
    end
  end
end

Testing Views

We could use a tool like Capybara to test client-side views and interactions (e.g. does clicking on "Logout" do what we expect?). We won't cover view testing today, though!

Maintaining tests

It's extremely important to maintain tests (especially on the master branch) and deal with test failures as soon as possible. If tests are left to languish until there are many failures, your tests lose their value and become untrustworthy. The investment your team made in testing is wasted.

Intermittent test failures are the bane of many a developers life. It's important to track these down too...they're usually caused by a poorly written test.

Other Tools

  • FactoryGirl
  • shoulda - Make Rails model tests super easy.
  • DatabaseCleaner - used to wipe the database before each test, not necessary on smaller apps as tests are rolled-back.

Challenges

Fork and clone the rspec testing app. Follow the instructions there.

About

Introduction to Unit testing with a link to a testing lab

Resources

Stars

Watchers

Forks

Releases

No releases published

Packages

No packages published