Ruby 社区一直都喜欢测试驱动开发,Rails 框架内置就有测试模块,本文将介绍如何利用 RSpec 来做单元测试,当然 RSpec 也可以用来做集成测试。

Rails 和 RSpec

Rails 项目中使用 RSpec,在 Gemfile 加入如下配置:

group :test do
 gem 'rspec-rails', '~> 3.1.0'
 gem 'factory_girl_rails'
end 

安装 Gems:

$ bundle install

初始化 spec 目录,spec 目录下会生成文件 spec_helper.rb,此文件将 RSpec 和 Rails 结合起来,运行 RSpec 测试的时候加载 Rails 项目等等这些:

$ rails generate rspec:install

Factory Girl

上一步配置 Gemfile 文件的时候,还声明了 factory_girl_rails,Factory Girl 作用是生成测试需要的数据,在测试很多问题的时候,都需要预先在数据库中填充一些数据,测试完一个单元,还应该清除数据库中的数据,保证下一个单元的测试不被这些遗留数据所影响,我们应该为每一个测试中要用到的 Model,在 spec/factories 编写一个 Factory:

# spec/factories/category.rb
FactoryGirl.define do
  factory :category do
    language "en"
    name "A Category"

    factory :food_category do
      is_food true
    end

    factory :exercise_category do
      is_food false
    end
  end
end

# spec/factories/exercise.rb
FactoryGirl.define do
  factory :exercise do
    language "en"
    name "A Exercise"
    association :category, :factory => :exercise_category
  end
end

测试 Models

在 spec/models 中编写针对 Models 的业务逻辑测试。

每一个锻炼都有一个分类,当分类下面的锻炼不为零的时候,分类是不能够被删除的。

接着我们来编写测试 spec/models/category_spec.rb 来保证上面这个需求:

require "spec_helper"

describe Category, :type => :model do
  it "can not remove category have exercises" do
    category = FactoryGirl.create(:exercise).category
    expect(category.destroy).to be false
    expect(category.errors.full_messages.first).to eql("Cannot destroy category when its exercises not zero")
  end
end

终端中运行测试,我们还没有实现这样的业务逻辑,肯定会报错:

$ bin/rspec spec/models/category_spec.rb

通过 before_destory 实现相应的业务逻辑,再次运行上面的测试,就会看到测试通过:

class Category < ActiveRecord::Base
  has_many :foods
  has_many :exercises

  validates :name, :language, :presence => true
  validates :name, :length => { :maximum => 23 }

  before_destroy do |category|
    if category.is_food
      unless category.foods.count == 0
        errors[:base] << I18n.t("errors.not_zero", :scope => :admin, :parent => I18n.t("models.category", :scope => :activerecord).downcase, :children => I18n.t("foods", :scope => :admin).downcase)
        false
      end
    else
      unless category.exercises.count == 0
        errors[:base] << I18n.t("errors.not_zero", :scope => :admin, :parent => I18n.t("models.category", :scope => :activerecord).downcase, :children => I18n.t("exercises", :scope => :admin).downcase)
        false
      end
    end
  end
end

测试 Controllers

在 spec/controllers 中编写针对 Controllers 的业务逻辑测试。

输入正确的用户名和密码就可以登录到管理页面,否则就不可以

接着我们来编写测试 spec/controllers/sessions_controller_spec.rb 来保证这个上面这个需求:

require "spec_helper"

describe SessionsController, :type => :controller do
  context "post create" do
    it "login as admin" do
      admin = FactoryGirl.create(:admin)
      post :create, { :username => admin.username, :password => admin.password }
      expect(response).to redirect_to(admin_root_path)
    end

    it "cannot login as normal user" do
      user = FactoryGirl.create(:user)
      post :create, { :username => user.username, :password => user.password }
      expect(response).to be_success
      expect(response.status).to eq(200)
      expect(flash[:alert]).to eql("Invalid username or password")
    end
  end
end

终端中运行测试,我们还没有实现这样的业务逻辑,肯定会报错:

$ bin/rspec spec/controllers/sessions_controller_spec.rb

通过 create 实现相应的业务逻辑,再次运行上面的测试,就会看到测试通过:

class SessionsController < ApplicationController
  layout false

  def create
    user = User.find_by_username_and_password_and_admin(params[:username], params[:password], true) 
    if user
      session[:login] = user.id
      redirect_to admin_root_path
    else
      flash.now[:alert] = I18n.t("session.error")
      render :new
    end
  end
end