Skip to content

Instantly share code, notes, and snippets.

@jnewman12
Last active September 25, 2015 02:08
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save jnewman12/8ad83cd5a8436457fde0 to your computer and use it in GitHub Desktop.
Save jnewman12/8ad83cd5a8436457fde0 to your computer and use it in GitHub Desktop.
Quick guide to get up to speed in Rspec

Getting set up and up to speed with Rspec

This is a quick guide to get up to speed with rspec in rails. This was made as a guide while building this repo for reference.

What testing accomplishes

Other than some of the more obvious benefits of testing, when you look at a rspec test suite, you see how things are named and modeled. For example, a test for a user model might look something like this

# sample user_spec.rb
describe User do
    it 'has a valid username' do
      expect(build(:user)).to be_valid
    end

    it 'is invalid without a username' do
      expect(build(:user, username: nil)).to_not be_valid
    end
end

If you read it, it looks like you're testing for User has a valid username, User is invalid without a username. This is the benefit of using rspec instead of Test::Case as it gives you human-like scenarios to test.

1. Set up

For the sample repo, here is the relevant testing gems I am using with a breakdown of each gem

# Gemfile
group :development, :test do
  gem 'byebug'
  gem 'web-console', '~> 2.0'
  gem 'spring'
  gem 'rspec-rails', '~> 3.0' # rspec rails library

  gem 'shoulda-matchers'
  gem 'factory_girl_rails'
  gem 'faker'
  gem 'capybara'
  gem 'selenium-webdriver'
  gem 'database_cleaner'
end

let's focus on lines 35-40

  • 35: Shoulda Matchers provides RSpec- and Minitest-compatible one-liners that test common Rails functionality.
  • 36: factory_girl is a fixtures replacement with a straightforward definition syntax, support for multiple build strategies and allows us to use this sample data for tests in our specs
  • 37: faker provides fake data so we dont have to write it
  • 38: capybara helps test web applications by simulating how a real user would interact with your app. This is especially important for feature tests
  • 39: selenium-webdriver allows for web browser automation. it is also good when running js in your specs
  • 40: database_cleaner cleans our data (after some configuration) after each run, so we dont get duplicated (which could fail certain validations)

after running bundle install, we then run rails generate rspec:install which generates our spec folder, and an .rspec file

the next piece is adjusting our rails_helper file which is what is configuring our rspec. Here's what mine looks like

# spec/rails_helper.rb
....
require 'capybara/rails'

config.mock_with :rspec

config.before(:suite) do
  DatabaseCleaner.strategy = :transaction
  DatabaseCleaner.clean_with(:truncation)
end

config.before(:each, :js => true) do
  DatabaseCleaner.strategy = :truncation
end

config.before(:each) do
  DatabaseCleaner.start
end

config.after(:each) do
  DatabaseCleaner.clean
end

this basically just sets up our dependencies from our gemfile, and lists capybara and database_cleaner.

2. Factory Set Up

The next part we should do is our factory set up, which will allow us to define what a sample object will look like, so we can reuse it when testing the rest of our app. Let's set up a user factory. In our spec folder, let's make a factories directory.

# spec/factories/user.rb
FactoryGirl.define do
  factory :user do
    username   { Faker::Internet.user_name }
    email    { Faker::Internet.email  }
    password    { Faker::Internet.password(8)  }
  end
end

In this, we took the attributes that are defined in our user, and plugged faker data in (but could be a simple string). This gives us a user factory which we can reuse. While I'm here, I might as well make my question and answer factories too

# spec/factories/question.rb
FactoryGirl.define do
  factory :question do
    title    { Faker::Company.catch_phrase  }
    body    { Faker::Company.bs  }
  end
end

# spec/factories/answer.rb
FactoryGirl.define do
  factory :answer do
    body    { Faker::Company.bs  }
  end
end

3. Model Specs

while we made our factories, let's test our model validations. In my 3 models, I added the following

# user.rb
class User < ActiveRecord::Base
	has_secure_password

	has_many :questions
	has_many :answers

	validates :username, presence: true
	validates :email, presence: true
end

# question.rb
class Question < ActiveRecord::Base
	belongs_to :user
	has_many :answers

	validates_presence_of :title
	validates_presence_of :body
end

# answer.rb
class Answer < ActiveRecord::Base
	belongs_to :user
	belongs_to :question

	validates_presence_of :body
end

now let's add our model specs, which are testing our validations

# spec/models/user_spec.rb
require 'rails_helper'

describe User do
    it 'has a valid username' do
      expect(build(:user)).to be_valid
    end

    it 'is invalid without a username' do
      expect(build(:user, username: nil)).to_not be_valid
    end

    it 'is invalid without an email' do 
    	expect(build(:user, email: nil)).to_not be_valid
    end

    it 'is invalid without matching passwords' do 
    	expect(build(:user, password: 'password', password_confirmation: 'password1')).to_not be_valid
    end
end

# spec/models/question_spec.rb
require 'rails_helper'

describe Question do 
	it 'has a valid title' do 
		expect(build(:question)).to be_valid
	end

	it 'is invalid without a title' do 
		expect(build(:question, title: nil)).to_not be_valid
	end

	it 'is invalid without a body' do 
		expect(build(:question, body: nil)).to_not be_valid
	end
end

# spec/models/answer_spec.rb
require 'rails_helper'

describe Answer do 
	it 'should be valid' do 
		expect(build(:answer)).to be_valid
	end

	it 'should not be valid without a body' do 
		expect(build(:answer, body: nil)).to_not be_valid
	end
end

now if we run rspec spec/models/ in our terminal we should see

Finished in 0.74634 seconds (files took 2.25 seconds to load)
9 examples, 0 failures

4. Controller Specs

Now that our models are working, let's test our controllers. A major feature of this app (and most in general) is sessions. Let's start with testing our sessions, and the functionality that comes with that. Let's start by writing our tests first

# spec/controller/sessions_controller_spec.rb
describe SessionsController do 
	describe 'POST #create' do
	  context 'when password is invalid' do
	    it 'renders the page with error' do
	      user = create(:user)

	      post :create, session: { email: user.email, password: 'invalid' }

	      expect(response).to render_template(:new)
	      expect(flash[:notice]).to match(/^Email and password do not match/)
	    end
	  end

	  context 'when password is valid' do
	    it 'sets the user in the session and redirects them to the root url' do
	      user = create(:user)

	      post :create, session: { email: user.email, password: user.password }

	      expect(controller.session[:user_id]).to eq @current_user 
	      expect(response).to have_http_status(200)
	    end
	  end
	end
end

here we are testing that when a user logs in with an invalid password that it will render a new signin form, and when a user signs in that we are expecting the status code to be a 200, which means when sending a post request that the user was created and is currently a current_user

here is the code for this test

# controller/sessions_controller.rb
class SessionsController < ApplicationController

	def new
	end

	def create
	   user = User.find_by_email(params[:email])
	   # If the user exists AND the password entered is correct.
	   if user && user.authenticate(params[:password])
	     # Save the user id inside the browser cookie. This is how we keep the user 
	     # logged in when they navigate around our website.
	     session[:user_id] = user.id
	     redirect_to root_path
	   else
	   # If user's login doesn't work, send them back to the login form.
	     render :new
	     flash.notice = 'Email and password do not match'
	   end
	 end

	def destroy
	   session[:user_id] = nil
	   redirect_to '/login'
	end
end

and if we go back in the terminal and run rspec spec/controllers/sessions_controller_spec.rb we see

Finished in 0.47184 seconds (files took 2.27 seconds to load)
2 examples, 0 failures

the rest of the controller specs are testing for CRUD operations on both of the models. If you would like to see the specs they are here

5. Feature Specs

The last part of this is testing our features. Other than sessions and auth which were tested above in our sessions controller and models, a feature of this app is to be able to upvote posts. Let's test for that.

For the model and controller specs, we were testing certain actions. We we're testing that things were being rendered, we are being redirected things are or are not valid, etc.

For the feature tests, we do something somewhat different. We are now testing scenarios that our users will see or interact with, and will be relying on a mix of rspec and capybara.

Although not complete, here is my feature spec for clicking the 'upvote' link in the index page

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

feature 'User can upvote question' do 
	scenario 'they see questions on the page' do 
		visit root_path
		expect(page).to have_content 'Example Q & A'
	end

	scenario 'they upvote a specific question' do
		visit root_path
		click_link("upvote")
		expect(page).to redirect_to '/'
	end 
end

I hope this served as a good guide to get started with. My only 'hesitation' with testing is so much of it is out of date, or can be in a different format, that it can be hard to get going with. I have found that the docs are solid, but even then they do not always have a correct answer.

links I liked

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment