Skip to content

# Step 13 — Rspec test Setup

Gerald Goh edited this page Jul 26, 2020 · 12 revisions

Add RSpec in gemfile

Ensure rspec-rails present in both the :development and :test groups of your app’s Gemfile:

/gemfile

group :development, :test do
  gem 'rspec-rails', '~> 4.0'
end

Install RSpec Gem

In app root directory run the following command in terminal

# Download and install
$ bundle update
$ bundle install
$ bundle update rspec-rails

# Generate boilerplate configuration files
# (check the comments in each generated file for more information)
$ rails generate rspec:install
      create  .rspec
      create  spec
      create  spec/spec_helper.rb
      create  spec/rails_helper.rb

Install RSpec user model

Create boilerplate specs with rails generate after coding is complete

$ rails generate rspec:model user

/rspec/models/user_spec.rb
require 'rails_helper'

RSpec.describe User, type: :model do
  let! (:users) { create_list(:user, 10) }

  it { should have_many(:readings) }
  it { should validate_presence_of(:name) }
  it { should validate_length_of(:name) }
  it { should validate_presence_of(:email) }
  it { should validate_uniqueness_of(:email) }
  it { should validate_presence_of(:password) }
  it { should validate_length_of(:password) }
  it { should validate_confirmation_of(:password) }
  it { should validate_presence_of(:units) }
  it { should validate_presence_of(:target) }
end

Run user model test with the following command in terminal

$ bundle exec rspec spec/models
.......

Finished in 0.26756 seconds (files took 2.63 seconds to load)
7 examples, 0 failures

Install RSpec Reading model

$ rails generate rspec:model reading
      create  spec/models/reading_spec.rb

require 'rails_helper'

RSpec.describe Reading, type: :model do
  let!(:user) { create(:user) }
  let!(:readings) { create_list(:reading, 1, user_id: user.id) }
  let(:user_id) { user.id }
  let(:id) { readings.first.id }

  it { should belong_to(:user) }
  it { should validate_presence_of(:bedroom) }
  it { should validate_presence_of(:study) }
  it { should validate_presence_of(:garage) }
  it { should validate_presence_of(:living) }
  it { should validate_presence_of(:kitchen) }
  it { should validate_presence_of(:living) }
  it { should validate_presence_of(:available) }
  it { should validate_presence_of(:saved) }
end

Run all model test with the following command in terminal

$ bundle exec rspec spec/models/reading_spec.rb
.........

Finished in 1.4 seconds (files took 2.95 seconds to load)
9 examples, 0 failures

RSpec for API request

Add gems

/gemfile

group :test do
  gem 'database_cleaner'
  gem 'factory_bot_rails'
  gem 'shoulda-matchers'
end

Create json helper

spec/support/request_spec_helper.rb

module RequestSpecHelper
  # Parse JSON response to ruby hash
  def json
    JSON.parse(response.body)
  end
end

Edit Rails helper

spec/railes_helper.rb

# require database cleaner at the top level
require 'database_cleaner'

# This file is copied to spec/ when you run 'rails generate rspec:install'
require 'spec_helper'
ENV['RAILS_ENV'] ||= 'test'

require File.expand_path('../config/environment', __dir__)

# Prevent database truncation if the environment is production
abort('The Rails environment is running in production mode!') if Rails.env.production?
require 'rspec/rails'
# Add additional requires below this line. Rails is not loaded until this point!

# Requires supporting ruby files with custom matchers and macros, etc, in
# spec/support/ and its subdirectories. Files matching `spec/**/*_spec.rb` are
# run as spec files by default. This means that files in spec/support that end
# in _spec.rb will both be required and run as specs, causing the specs to be
# run twice. It is recommended that you do not name files matching this glob to
# end with _spec.rb. You can configure this pattern with the --pattern
# option on the command line or in ~/.rspec, .rspec or `.rspec-local`.
#
# The following line is provided for convenience purposes. It has the downside
# of increasing the boot-up time by auto-requiring all files in the support
# directory. Alternatively, in the individual `*_spec.rb` files, manually
# require only the support files necessary.
#
# Dir[Rails.root.join('spec', 'support', '**', '*.rb')].each { |f| require f }

# Checks for pending migrations and applies them before tests are run.
# If you are not using ActiveRecord, you can remove these lines.
begin
  ActiveRecord::Migration.maintain_test_schema!
rescue ActiveRecord::PendingMigrationError => e
  puts e.to_s.strip
  exit 1
end

Dir[Rails.root.join('spec/support/**/*.rb')].sort.each { |f| require f }

RSpec.configure do |config|
  config.include RequestSpecHelper, type: :request
  # add `FactoryBot` methods
  config.include FactoryBot::Syntax::Methods

  # start by truncating all the tables but then use the faster transaction strategy the rest of the time.
  config.before(:suite) do
    DatabaseCleaner.clean_with(:truncation)
    DatabaseCleaner.strategy = :transaction
  end

  # start the transaction strategy as examples are run
  config.around(:each) do |example|
    DatabaseCleaner.cleaning do
      example.run
    end
  end

  # Remove this line if you're not using ActiveRecord or ActiveRecord fixtures
  config.fixture_path = "#{::Rails.root}/spec/fixtures"

  # If you're not using ActiveRecord, or you'd prefer not to run each of your
  # examples within a transaction, remove the following line or assign false
  # instead of true.
  config.use_transactional_fixtures = true

  # RSpec Rails can automatically mix in different behaviours to your tests
  # based on their file location, for example enabling you to call `get` and
  # `post` in specs under `spec/controllers`.
  #
  # You can disable this behaviour by removing the line below, and instead
  # explicitly tag your specs with their type, e.g.:
  #
  #     RSpec.describe UsersController, :type => :controller do
  #       # ...
  #     end
  #
  # The different available types are documented in the features, such as in
  # https://relishapp.com/rspec/rspec-rails/docs
  config.infer_spec_type_from_file_location!

  # Filter lines from Rails gems in backtraces.
  config.filter_rails_from_backtrace!
  # arbitrary gems may also be filtered via:
  # config.filter_gems_from_backtrace("gem name")
end

# configure shoulda matchers to use rspec as the test framework and full matcher libraries for rails
Shoulda::Matchers.configure do |config|
  config.integrate do |with|
    with.test_framework :rspec
    with.library :rails
  end
end

Create RSpec for users controller

$ rails g rspec:controller users
      create  spec/requests/users_request_spec.rb

Build user factory

spec/factories/users.rb

FactoryBot.define do
  factory :user do
    name { Faker::Name.name }
    email { Faker::Internet.email }
    password { "somepassword" }
    password_confirmation { "somepassword"}
    units { 1800 }
    target { 5 }
  end
end

Build Test cases for users

spec/requests/user_request_spec.rb

require 'rails_helper'

RSpec.describe "Users", type: :request do
  let! (:users) { create_list(:user, 15) }
  let (:user_id) { users.last.id }

  describe 'Get /api/v1/users' do
    before { get '/api/v1/users' }

    it 'return all users' do
      expect(json).not_to be_empty
      expect(json["data"].size).to eq(15)
    end

    it 'return status code 200' do
      expect(response).to have_http_status(200)
    end
  end

  describe 'GET /api/v1/users/:id' do
    before { get "/api/v1/users/#{user_id}" }

    context 'when user exists' do
      it 'resturns the user' do
        expect(json).not_to be_empty
        expect(json["data"]["id"]).to eq(user_id)
      end

      it 'returns status code 200' do
        expect(response).to have_http_status(200)
      end
    end
  end
  context 'when user does not exist' do
    let(:user_id) { 9 }
    it 'returns nil' do
      expect(response).to eq(nil)
    end
  end
end

describe 'POST /api/v1/users' do
  let! (:valid_attributes) { {user: { name: 'Bruce tester', email: 'tester@email.com', password: 'password', password_confirmation: 'password', units: 1800, target: 5 } } }

  context 'when the request is valid' do
    before { post '/api/v1/users', params: valid_attributes }

    it 'returns status code 200 when request is accepted' do
      expect(response).to have_http_status(200)
    end
  end

  context 'when the request is invalid' do
    before { post '/api/v1/users', params: { user: { name: ' ', email: ' ', password: 'password', password_confirmation: 'password', units: 1800, target: 5 } } }

    it 'return status code 400' do
      expect(json["code"]).to eq(400)
    end

    it 'returns a validation error message' do
      expect(response.body).to match(/can't be blank/)
    end
  end
end

Build Reading factory

spec/factories/reading.rb

FactoryBot.define do
  factory :reading do
    bedroom { Faker::Number.between(from: 1, to: 10) }
    study { Faker::Number.between(from: 1, to: 10) }
    garage { Faker::Number.between(from: 1, to: 10) }
    living { Faker::Number.between(from: 1, to: 10) }
    kitchen { Faker::Number.between(from: 1, to: 10) }
    guest { Faker::Number.between(from: 1, to: 10) }
    consumption { Faker::Number.between(from: 1, to: 10) }
    available { Faker::Number.between(from: 1, to: 10) }
    saved { Faker::Number.between(from: 1, to: 10) }
    user_id { nil }  
  end
end

Create RSpec for readings controller

$ rails g rspec:controller readings
      create  spec/requests/readings_request_spec.rb
spec/requests/readings_requests_spec.rb

require 'rails_helper'

RSpec.describe "Readings", type: :request do
  let!(:user) { create(:user) }
  let!(:readings) { create_list(:reading, 2, user_id: user.id) }
  let(:user_id) { user.id }
  let(:id) { readings.first.id }

  describe 'POST /api/v1/readings' do
    let! (:valid_attributes) { { bedroom: 1, study: 1, garage: 1, living: 1, kitchen: 1, guest: 1, consumption: 1, available: 1, saved: 1, user_id: 1 } }
  
    context 'when the readings creation' do
      before { post '/api/v1/readings', params: valid_attributes }
  
      it 'returns error code 401 before login' do
        expect(json["code"]).to eq(401)
      end
    end
    context 'when the request is invalid' do
      before { post '/api/v1/readings', params: { bedroom: ' ', study: ' ', garage: 1, living: 1, kitchen: 1, guest: 1, consumption: 1, available: 1, saved: 1, user_id: 1 } }
  
      it 'return status code 401 before login' do
        expect(json["code"]).to eq(401)
      end
    end
  end

  describe 'GET /api/v1/readings' do
    before { get "/api/v1/readings" }

    context 'when all readings are hidden before required login' do
      it 'returns error code 401' do
        expect(json["code"]).to eq(401)
      end
    end
  end
  
  describe 'GET /api/v1/readings/user/:user_id' do
    before { get "/api/v1/readings/user/#{user_id}" }

    context 'when users readings are hidden before required login' do
      it 'returns error code 401' do
        expect(json["code"]).to eq(401)
      end
    end
    context 'when user does not exists' do
      let(:user_id) { 0 }

      it 'returns status code 401' do
        expect(json["code"]).to eql(401)
      end
    end
  end

  describe 'GET /api/v1/user/:user_id/reading/:id' do
    before { get "/api/v1/user/#{user_id}/reading/#{id}" }

    context 'when user single reading is hidden before required login' do
      it 'returns status code 401' do
        expect(json["code"]).to eq(401)
      end

      context 'when user does not exists' do
        let(:user_id) { 0 }
  
        it 'returns status code 401' do
          expect(json["code"]).to eql(401)
        end
      end
    end
  end
end