Developing Rails app with RSpec

This tutorial is for user who want developing rails app with RSpec.

refered to this site

Using

  • Rails 4.0.4
  • Ruby 2.0.0
  • mysql2 0.3.18

Gemfile

group :development, :test do
gem 'rspec-rails'
gem 'factory_girl_rails'
end

group :test do
gem 'faker'
gem 'capybara'
gem 'guard-rspec'
gem 'launchy'
end

Setting test database

edit your config/database.yml

MySQL

test:
adapter: mysql2
encoding: utf-8
reconnect: false
database: [[ your-database-name ]]_test
pool: 5
username: [[ your-database-username ]]
password: [[ if you use password ]]
host: 127.0.0.1 # if you want use remote mysql server, replace to remote ip
port: 3306
timeout: 5000

RSpec Configuration

after run bundle install start rspec setting.

$ rails g rspec:install

this command initialize spec/ folder.

and edit .rspecfile in your project folder for easy to see result.

--format documentation

Generators

edit config/application.rb and add following code.

config.generators do |g|
g.test_framework :rspec,
  fixtures: true,
  view_specs: false,
  helper_specs: false,
  routing_specs: false,
  controller_specs: true,
  request_specs: true
  g.fixture_replacement :factory_girl, dir: 'spec/factories'
  end

when you make models or controllers(like rails g model user), spec file made automatically.

Model test

When we make model, add some validates and associations.

Now we check validates, has correct associations doing well.

Validates

Assume that we make a blog app and want to make User model.

We think that we want user’s email and password are presence, email is uniqueness and password is have minimum 6 words. Yes, this is model validates.

it’s very simple. open app/models/user.rb and add following codes.

class User < ActiveRecord::Base
validates :email, :password, presence: true
validates :email, uniqueness: true
validates :password, length: { minimum: 6 }
end

Before test, make test data using FactoryGirl.

open spec/factories/user.rb and add following codes.

FactoryGirl.define do
factory :wonjae, class: User do |f|
  f.email "test@test.com"
  f.password "99e193ca18"
end

factory :minsoo, class: User do |f|
  f.email "test2@tester.com"
  f.password "992193ca18"
  end
end

When we run @user = FactoryGirl.create(:wonjae) @user have email “test@test.com” and password “99e193ca18”. it’s very good for testing.

Now we should test this validates. open spec/models/user_spec.rb and make test.

require 'rails_helper'
require 'spec_helper'

RSpec.describe User, type: model do
  context '#create' do
    it 'email should presence' do
      expect(FactoryGirl.create(:wonjae, email: '')).not_to be_valid
    end

    it 'password should presence' do
      expect(FactoryGirl.create(:wonjae, password: '')).not_to be_valid
    end

    it 'email should unique' do
      FactoryGirl.create(:wonjae)

      expect(FactoryGirl.build(:wonjae)).not_to be_valid
    end
  end
end

Tips

If you want to use guard-rails, execute command guard in shell, guard-rails is run.
When you change your codes, guard run test automatically. It’s very useful when you write test codes.
else, execute command rspec spec/models/user_spec.rb run test also. I recommend first.

Associations

In blog, one user can have many posts and one post can have many comments. Yes, it’s 1:N associations!
Now we test User model has proper association with Post.

Add following codes in app/models/user.rb

has_many :posts, dependent: :destroy

This code means that User can have many posts and when delete user, also delete posts too.

And add following codes in app/models/post.rb

Before

If you didn’t create Post model, run rails g model post body:string user_id:integer and rake db:migrate

class Post < ActiveRecord::Base
  belongs_to :user
end

Now we make associations before we talk. So it’s test time!

open spec/models/user_spec.rb

context 'associations' do
  it 'can have many posts' do
    as = User.reflect_on_associations(:posts)
    expect(as.macro).to eq(:has_many)
  end
end

and open spec/models/post_spec.rb

context 'associations' do
  it 'belongs to user' do
    as = Post.reflect_on_associations(:user)
    expect(as.macro).to eq(:belongs_to)
  end
end

These tests are doing well. But you want see associations doing really well, run rails c, make user instance user and user.posts returns ActiveRecord associations.

Controller test

Before we test model validates, associations. If model consist of validates and associations, controller consist of actions and logic.

So in controller test, we test actions and logic.

Actions

Before we assume that we make blog app. So we need to login feature. This is action.

Let’s make login controller. run rails g controller login

Before

In login controller, we return result using json. So in test we parse json.

Assume that what happens when we login? Hum.. write wrong email or password or submit form before write email or password, even we didn’t register but try to login! So we need to prevent these unexpected actions.

Now make tests for test these unexpected actions.

Open spec/controllers/login_controller.rb and add following codes.

require 'rails_helper'
require 'spec_helper'

RSpec.describe LoginController, type: :controller do
  before(:each) do
    @email = 'test@test.com'
    @password = 'asdfaa'

    User.create(email: @email, password: @password, username: 'akkiros')
  end

  describe '#login' do
    it 'email is nil' do
      post :login, { password: @password }

      body = JSON.parse(response.body)

      expect(body['success']).to eq(false)
    end

    it 'password is nil' do
      post :login, { email: @email }

      body = JSON.parse(response.body)

      expect(body['success']).to eq(false)
    end

    it 'not match user' do
      post :login, { email: 't@test.com', password: @password }

      body = JSON.parse(response.body)

      expect(body['success']).to eq(false)
    end

    it 'not match password' do
      post :login, { email: @email, password: @password }

      body = JSON.parse(response.body)

      expect(body['success']).to eq(false)
    end

    it 'login success' do
      post :login, { email: @email, password: @password }

      body = JSON.parse(response.body)

      expect(body['success']).to eq(true)
    end
  end
end

Tips

We test one action, but tests are so long. Because tests are test all possible cases. See this link

In login controller test, we test cases. So we make login controller suitable actions.

Open app/controllers/login_controller.rb and add following codes.

class LoginController < ApplicationController
  def initialize
    @result = {}
  end

  def login
    email = params[:email] unless params[:email].nil?
    password = params[:password] unless params[:password].nil?

    return error('Argument Error') if email.nil? || password.nil?

    @user = User.find_by_email(email)

      return error('No match user') if @user.nil?
      return error('Password is not match') unless @user.password == password

      success
  end

  private

  def error(message)
    @result = {
      success: false,
      error: {
        message: message,
       }
    }

    render json: @result
  end

  def success
    render json: @result.merge!(success: true)
  end
end

This login controller return json type.
If login controller have not enough params, it returns error with Argument Error message.
Similarly no matching user by email, it returns error with No match user message.

And add routes. Edit config/routes.rb

post '/login' => 'login#login'

Now run rspec spec/controllers/login_controller_spec.rb all tests ok!

Author

Wonjae Kim / @Akkiros