Skip to content

SF-WDI-LABS/testing-with-rspec

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

14 Commits
 
 

Repository files navigation

Testing Rails with RSpec

Why is this important?

This workshop is important because:

Rails apps tend to be large and complex. All the benefits of testing are even more apparent with these apps, and the community has created gems that make it easier to test Rails with the Rspec gem we've used before.

What are the objectives?

After this workshop, developers will be able to:

  • Identify various aspects of Rails apps that we might want to test.
  • Test model methods using rspec-rails.
  • Test controller actions using rspec-rails.

Where should we be now?

Before this workshop, developers should already be able to:

  • Explain the difference between unit and integration tests.
  • Describe the roles of routes, controllers, views, and models in a Rails app.

Resources

Resource Description
RSpec matchers Reference for RSpec
shoulda matchers Dev-friendly methods 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 its 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 code can be depoloyed, or merged into master, the entire test suite is run and all tests must pass.

rspec

RSpec is a testing gem for Ruby. It helps us write tests that sound like user stories or planning comments ("describe add; it ... ").

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.

Testing Best Practices

We try to test each component or piece independently.

Test organization:

  • create a test file for each class
  • add a group of tests for each method that needs to be tested in the class
  • if methods should behave very differently in different scenarios, use context to group tests for each scenario (logged in vs logged out, valid data vs invalid, etc)
  • write a test for each behavior the method should do

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.

rspec-rails

rspec-rails is a testing framework built on rspec, specifically for Rails. We'll use rspec-rails to test our models and controllers.

rspec-rails helps us implement a 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

Most of the methods we'll use, including setup methods like let, let!, before, and subject come from rspec; they're not limited to rspec-rails.

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 these and others:

  • spec/models/user_spec.rb
  • spec/controllers/users_controller_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 'featured'

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

Writing rspec-rails Tests

rspec-rails gives us structures for every phase of a test: setup, definition & exercise, validate, and teardown steps.

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

Testing Rails Models

When thinking about models, there are a few things you might want to test. In Rails, the most common testing targets are:

  • built-in validations
  • custom validations
  • custom model methods
  • associations

Below is a partial test file for a User model. It sets up a user instance with first and last names (see "setup" section above). Then, it tests that a full_name custom model method correctly calculates the full name:

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

  ...

  
  describe "#full_name" do
    subject(:user) { User.create(first_name: 'Juan', last_name: 'Grey') }
    
    it "joins first name and last name" do
      expect(user.full_name).to eq("Juan Grey")
    end
  end

end
Check for Understanding

Go through this code line-by-line with a partner. Alternate which partner explians each line.

What steps will developers need to do to make this test pass?
  1. Make sure there is a User model.
  2. Define a full_name instance method in the user model class.
  3. Make sure instances of user have a first_name and a last_name so the setup works as expected.
  4. Fill in the full_name method so that it returns the correct string.
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

  ...
  # without Factory Girl
  subject(:user) { User.create(first_name: 'Juan', last_name: 'Grey') }
  OR 
  # FactoryGirl version (requries factory, next snippet)
  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 is random. It can make it hard to predict what data you should see, and you might run into intermittent issues because of duplicate data or unexpected random results. Therefore many developers prefer to hard-code test data or use something like FactoryGirl's sequence.

shoulda matchers

Previously we talked about model validations. You can test these. The shoulda matchers gem provides an easy 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 matchers:

it { should validate_presence_of(:title) }
# another syntax:
it { is_expected.to validate_presence_of(:title) }

Testing Controllers

To test authentication, we need to define a current_user before each of our tests run.

#
# 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
Check for Understanding
  1. What does allow_any_instance_of(ApplicationController).to receive(:current_user).and_return(signed_in_user) mean (the last line of the before block)?
click for information

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 signed_in_user variable we made with FactoryGirl.

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.

Resources

Releases

No releases published

Packages

No packages published