$ echo 'gem "rspec-rails", group: [:development, :test]' >> Gemfile
$ bundle install
$ rails generate rspec:install
$ rails spec
$ rspec spec --format documentation
# spec/failing_spec.rb
require "rails_helper"
# 配置
RSpec.configure do |config|
config.xxx
...
end
# 类级别命名空间
RSpec.describe "类名", :type => :类型 do
# 使用内部的匿名控制器
controller do
def 方法名
# 实现
end
end
before(:context) do
@widget = Widget.create!
end
after(:context) do
@widget.destroy
end
# 类的方法级别命名空间
# describe "请求方法 方法名"
describe "GET index" do
before(:context) do
@widget = Widget.create!
end
after(:context) do
@widget.destroy
end
# 具体的测试用例 允许多个,包含成功的和失败的
it "raises an error" do
# 具体实现
get :index
end
end
end
$ rails generate rspec:model user
$ rails generate rspec:controller api::v1::users
生成文件 spec/models/user_spec.rb
更多的 specs 标签支持:
scaffold
model
controller
rails g rspec:controller api::v1::users
helper
view
mailer
integration
feature
job
channel
generator
mailbox
request
system
rspec 默认开启事务,这样可以保证测试完成后自动清空数据库。当然可以使用其他 gem 来完成这个功能,例如 database_cleaner 。
当运行 rails generate rspec:install
, 在文件 spec/rails_helper.rb
中默认引入了系列配置:
RSpec.configure do |config|
config.use_transactional_fixtures = true
end
RSpec.configure do |config|
config.use_transactional_fixtures = false
end
before
关键字实现自动回滚的控制自动回滚:任何在 before(:example)
定义的数据在执行完测试后都会自动清空。
describe Widget do
before(:example) do
@widget = Widget.create
end
it "does something" do
expect(@widget).to do_something
end
it "does something else" do
expect(@widget).to do_something_else
end
end
@widget
在每个 it 中都会被重新创建,所以互不干扰。
手动控制:在 before(:context)
中创建的数据则不会自动回滚。
你可以利用before(:context)
创建多个方法的公用数据。下面是几个原则:
一定要定义 after(:context)
来手动删除数据:
before(:context) do
@widget = Widget.create!
end
after(:context) do
@widget.destroy
end
否则你的数据会一直存在有可能影响其他测试。
一定要使用 before(:example)
刷新数据。
before(:context) do
@widget = Widget.create!
end
before(:example) do
@widget.reload
end
模块 | 所在目录 |
---|---|
Model specs |
spec/models |
Controller specs |
spec/controllers |
Request specs |
spec/requests 或者 integration 或者api
|
Feature specs |
spec/features |
View specs |
spec/views |
Helper specs |
spec/helpers |
Mailer specs |
spec/mailers |
Routing specs |
spec/routing |
Job specs |
spec/jobs |
System specs |
spec/system |
当然开发人员可以任意自定义目录结构,这个时候就需要明确的说明当前的测试是针对那个模块儿。如何指定呢?使用type
, 在 rspec 中可用的 type 类型有:
模块 | 使用类型标示 |
---|---|
Model specs |
type: :model |
Controller specs |
type: :controller |
Request specs |
type: :request |
Feature specs |
type: :feature |
View specs |
type: :view |
Helper specs |
type: :helper |
Mailer specs |
type: :mailer |
Routing specs |
type: :routing |
Job specs |
type: :job |
System specs |
type: :system |
举个例子:
# spec/legacy/things_controller_spec.rb
# 明确表明了要测试的类型
RSpec.describe ThingsController, type: :controller do
describe "GET index" do
# Examples
end
end
如果目录的安排是按照 rspec 规范的目录,可以通过配置自动的识别类型
# spec/rails_helper.rb
RSpec.configure do |config|
config.infer_spec_type_from_file_location!
end
但是如果是自定义目录,也可以这样做:
# set `:type` for serializers directory
RSpec.configure do |config|
config.define_derived_metadata(:file_path => Regexp.new('/spec/serializers/')) do |metadata|
metadata[:type] = :serializer
end
end
强烈推荐按照规范罗列目录,例如:
app
├── controllers
│ ├── application_controller.rb
│ └── books_controller.rb
├── helpers
│ ├── application_helper.rb
│ └── books_helper.rb
├── models
│ ├── author.rb
│ └── book.rb
└── views
├── books
└── layouts
lib
├── country_map.rb
├── development_mail_interceptor.rb
├── enviroment_mail_interceptor.rb
└── tasks
└── irc.rake
spec
├── controllers
│ └── books_controller_spec.rb
├── country_map_spec.rb
├── features
│ └── tracking_book_delivery_spec.rb
├── helpers
│ └── books_helper_spec.rb
├── models
│ ├── author_spec.rb
│ └── book_spec.rb
├── rails_helper.rb
├── requests
│ └── books_spec.rb
├── routing
│ └── books_routing_spec.rb
├── spec_helper.rb
├── tasks
│ └── irc_spec.rb
└── views
└── books
#spec/functional/widgets_controller_spec.rb
require "rails_helper"
RSpec.describe WidgetsController, :type => :controller do
it "responds successfully" do
get :index
expect(response.status).to eq(200)
end
end
#spec/ledger/entry_spec.rb
require "spec_helper"
Entry = Struct.new(:description, :us_cents)
RSpec.describe Entry do
it "has a description" do
is_expected.to respond_to(:description)
end
end
#spec/controllers/widgets_controller_spec.rb
require "rails_helper"
RSpec.configure do |config|
config.infer_spec_type_from_file_location!
end
RSpec.describe WidgetsController do
it "responds successfully" do
get :index
expect(response.status).to eq(200)
end
end
# spec/routing/duckduck_routing_spec.rb
require "rails_helper"
Rails.application.routes.draw do
get "/example" => redirect("http://example.com")
end
RSpec.configure do |config|
config.infer_spec_type_from_file_location!
end
# Due to limitations in the Rails routing test framework, routes that
# perform redirects must actually be tested via request specs
RSpec.describe "/example", :type => :request do
it "redirects to example.com" do
get "/example"
expect(response).to redirect_to("http://example.com")
end
end
Rails 中有些 gem 会让失败输出的 backtrace 信息会被过滤和简化,我们可以通过配置让失败的输出全部的 backtrace:
RSpec.configure do |config|
config.filter_rails_from_backtrace!
end
filter_rails_from_backtrace!
# spec/failing_spec.rb
require "rails_helper"
RSpec.configure do |config|
config.filter_rails_from_backtrace!
end
RSpec.describe "Controller", :type => :controller do
# 使用内部的匿名控制器
controller do
def index
raise "Something went wrong."
end
end
describe "GET index" do
it "raises an error" do
get :index
end
end
end
模型的 rspec 是 ActiveSupport :: TestCase 的美化包装,并且包括所有它提供的行为和断言
,以及 RSpec 自身的行为和期望
。
例子:
require "rails_helper"
# 模型是:Post, 明确指定了type
RSpec.describe Post, :type => :model do
# 命名空间是 with 2 or more comments
context "with 2 or more comments" do
# 真正的测试
it "orders them in reverse chronologically" do
# 调用create方法创建了一个 post
post = Post.create!
# 创建一个回复
comment1 = post.comments.create!(:body => "first comment")
# 创建第二个回复
comment2 = post.comments.create!(:body => "second comment")
# 期望所有回复中 comment2, comment1相等
expect(post.reload.comments).to eq([comment2, comment1])
end
end
end
# spec/models/widget_spec.rb
require "rails_helper"
# 验证模型 Widget
RSpec.describe Widget, :type => :model do
# 没有数据
it "has none to begin with" do
expect(Widget.count).to eq 0
end
# 添加一个
it "has one after adding one" do
Widget.create
expect(Widget.count).to eq 1
end
# 因为有事务 所以这个数据是独立的 也成立
it "has none after one was created in a previous example" do
expect(Widget.count).to eq 0
end
end
运行测试:
$ rspec spec/models/widget_spec.rb
应该都通过。
#spec/models/widget_spec.rb
require "rails_helper"
RSpec.configure do |c|
c.use_transactional_examples = true
end
RSpec.describe Widget, :type => :model do
it "has none to begin with" do
expect(Widget.count).to eq 0
end
it "has one after adding one" do
Widget.create
expect(Widget.count).to eq 1
end
it "has none after one was created in a previous example" do
expect(Widget.count).to eq 0
end
end
运行测试:
$ rspec spec/models/widget_spec.rb
应该都通过。
#spec/models/widget_spec.rb" with:
require "rails_helper"
RSpec.configure do |c|
# 关闭事务
c.use_transactional_examples = false
# 设定公用值
c.order = "defined"
end
RSpec.describe Widget, :type => :model do
it "has none to begin with" do
expect(Widget.count).to eq 0
end
it "has one after adding one" do
Widget.create
expect(Widget.count).to eq 1
end
it "has one after one was created in a previous example" do
expect(Widget.count).to eq 1
end
# 利用after删除
after(:all) { Widget.destroy_all }
end
运行测试:
$ rspec spec/models/widget_spec.rb
应该都通过。
#spec/models/thing_spec.rb
require "rails_helper"
RSpec.describe Thing, :type => :model do
fixtures :things
it "fixture method defined" do
things(:one)
end
end
#spec/fixtures/things.yml
one:
name: MyString
运行测试:
$ rspec spec/models/thing_spec.rb
应该都通过。
默认情况下,rspec 验证的 double 不支持动态方法。而 instance_double.rspec-rails 通过扩展的方法为列启用了此支持。
例子:
# spec/models/widget_spec.rb
require "rails_helper"
RSpec.describe Widget, :type => :model do
it "has one after adding one" do
# 假造一个对象Widget 属性为name
instance_double("Widget", :name => "my name")
end
end
controller spec 继承自 ActionController::TestCase::Behavior
, 允许你发送 http 请求并获取相应的期望:
可以像这样制定结果:
标准匹配: (expect(response.status).to eq(200)
)
标准断言: (assert_equal 200, response.status
)
rails 断言 (assert_response 200
)
rails-specific 匹配器:
expect(response).to render_template(:new) # wraps assert_template
expect(response).to redirect_to(location) # wraps assert_redirected_to
expect(response).to have_http_status(:created)
expect(assigns(:widget)).to be_a_new(Widget)
简单实例:
RSpec.describe TeamsController do
describe "GET index" do
it "assigns @teams" do
team = Team.create
get :index
expect(assigns(:teams)).to eq([team])
end
it "renders the index template" do
get :index
expect(response).to render_template("index")
end
end
end
指定头信息
require "rails_helper"
RSpec.describe TeamsController, type: :controller do
describe "GET index" do
it "returns a 200" do
# 添加头信息
request.headers["Authorization"] = "foo"
# 请求show方法
get :show
# 期待返回值是 200
expect(response).to have_http_status(:ok)
end
end
end
# spec/controllers/widgets_controller_spec.rb
require "rails_helper"
RSpec.describe WidgetsController, :type => :controller do
describe "GET index" do
it "has a 200 status code" do
get :index
# 期待状态码的值是 200
expect(response.status).to eq(200)
end
end
end
运行测试
$ rspec spec
all 通过测试
# spec/controllers/widgets_controller_spec.rb
require "rails_helper"
RSpec.describe WidgetsController, :type => :controller do
describe "responds to" do
it "responds to html by default" do
post :create, :params => { :widget => { :name => "Any Name" } }
# 返回值为类型为 "text/html"
expect(response.content_type).to eq "text/html"
end
it "responds to custom formats when provided in the params" do
post :create, :params => { :widget => { :name => "Any Name" }, :format => :json }
# 返回值为类型为 "text/html"
expect(response.content_type).to eq "application/json"
end
end
end
运行测试
$ rspec spec
all 通过测试
# spec/controllers/widgets_controller_spec.rb
require "rails_helper"
RSpec.describe WidgetsController, :type => :controller do
describe "responds to" do
it "responds to html by default" do
post :create, :params => { :widget => { :name => "Any Name" } }
expect(response.content_type).to eq "text/html"
end
it "responds to custom formats when provided in the params" do
# 设定请求参数类型为 json
post :create, :params => { :widget => { :name => "Any Name" }, :format => :json }
expect(response.content_type).to eq "application/json"
end
end
end
运行测试
$ rspec spec
all 通过测试
#spec/controllers/widgets_controller_spec.rb
require "rails_helper"
RSpec.describe WidgetsController, :type => :controller do
describe "responds to" do
it "responds to html by default" do
post :create, :params => { :widget => { :name => "Any Name" } }
# 编码类型为 text/html; charset=utf-8
expect(response.content_type).to eq "text/html; charset=utf-8"
end
it "responds to custom formats when provided in the params" do
post :create, :params => { :widget => { :name => "Any Name" }, :format => :json }
# 编码类型为 application/json; charset=utf-8
expect(response.content_type).to eq "application/json; charset=utf-8"
end
end
end
运行测试
$ rspec spec
all 通过测试
# spec/controllers/widgets_controller_spec.rb
require "rails_helper"
RSpec.describe WidgetsController, :type => :controller do
describe "responds to" do
it "responds to html by default" do
post :create, :params => { :widget => { :name => "Any Name" } }
#media_type 为 text/html
expect(response.media_type).to eq "text/html"
end
it "responds to custom formats when provided in the params" do
post :create, :params => { :widget => { :name => "Any Name" }, :format => :json }
expect(response.media_type).to eq "application/json"
end
end
end
运行测试
$ rspec spec
all 通过测试
# spec/controllers/widgets_controller_spec.rb
require "rails_helper"
RSpec.describe WidgetsController, :type => :controller do
describe "index" do
it "renders the index template" do
get :index
# 测试渲染了指定的 index 模板
expect(response).to render_template("index")
# 测试返回值为 “”
expect(response.body).to eq ""
end
it "renders the widgets/index template" do
get :index
expect(response).to render_template("widgets/index")
expect(response.body).to eq ""
end
end
end
运行测试
$ rspec spec
all 通过测试
# spec/controllers/widgets_controller_spec.rb
require "rails_helper"
RSpec.describe WidgetsController, :type => :controller do
describe "index" do
it "renders the 'new' template" do
get :index
expect(response).to render_template("new")
end
end
end
运行测试
$ rspec spec
没有通过测试
# spec/controllers/things_controller_spec.rb
require "rails_helper"
RSpec.describe ThingsController, :type => :controller do
describe "custom_action" do
it "renders an empty custom_action template" do
# 更改视图路径
controller.prepend_view_path 'app/views'
controller.append_view_path 'app/views'
get :custom_action
expect(response).to render_template("custom_action")
expect(response.body).to eq ""
end
end
end
#app/controllers/things_controller.rb
class ThingsController < ActionController::Base
layout false
def custom_action
end
end
#app/views/things/custom_action.html.erb
# 空文件
运行测试
$ rspec spec
all 通过测试
# spec/controllers/things_controller_spec.rb
require "rails_helper"
RSpec.describe ThingsController, :type => :controller do
render_views
it "renders the real custom_action template" do
controller.prepend_view_path 'app/views'
get :custom_action
expect(response).to render_template("custom_action")
expect(response.body).to match(/template for a custom action/)
end
end
运行测试
$ rspec spec
all 通过测试
# spec/controllers/widgets_controller_spec.rb
require "rails_helper"
RSpec.describe WidgetsController, :type => :controller do
render_views
describe "GET index" do
it "has a widgets related heading" do
get :index
expect(response.body).to match /<h1>.*widgets/im
end
end
end
运行测试
$ rspec spec
all 通过测试
# spec/controllers/widgets_controller_spec.rb
require "rails_helper"
RSpec.describe WidgetsController, :type => :controller do
context "with render_views" do
render_views
describe "GET index" do
it "renders the actual template" do
get :index
expect(response.body).to match /<h1>.*widgets/im
end
end
context "with render_views(false) nested in a group with render_views" do
render_views false
describe "GET index" do
it "renders the RSpec generated template" do
get :index
expect(response.body).to eq("")
end
end
end
end
context "without render_views" do
describe "GET index" do
it "renders the RSpec generated template" do
get :index
expect(response.body).to eq("")
end
end
end
context "with render_views again" do
render_views
describe "GET index" do
it "renders the actual template" do
get :index
expect(response.body).to match /<h1>.*widgets/im
end
end
end
end
运行测试
$ rspec spec --order default --format documentation
WidgetsController
with render_views
GET index
renders the actual template
with render_views(false) nested in a group with render_views
GET index
renders the RSpec generated template
without render_views
GET index
renders the RSpec generated template
with render_views again
GET index
renders the actual template
all 通过测试
# spec/support/render_views.rb" with:
RSpec.configure do |config|
config.render_views
end
# spec/controllers/widgets_controller_spec.rb
require "rails_helper"
require "support/render_views"
RSpec.describe WidgetsController, :type => :controller do
describe "GET index" do
it "renders the index template" do
get :index
expect(response.body).to match /<h1>.*widgets/im
end
end
end
运行测试
$ rspec spec
all 通过测试
使用 controller 方法定义一个匿名控制器,该匿名控制器将从传入的参数所描述的类继承。这对于指定类似全局错误处理的行为很有用。
要指定其他基类,您可以将该类显式传递给 controller 方法:
controller(BaseController)
您还可以禁用基本类型推断,在这种情况下,匿名控制器将默认继承自 ApplicationController 方法而不是所描述的类。
RSpec.configure do |c|
# 禁用基本类型推断
c.infer_base_class_for_anonymous_controllers = false
end
RSpec.describe BaseController, :type => :controller do
# ApplicationController
controller do
def index; end
# this normally creates an anonymous `BaseController` subclass,
# however since `infer_base_class_for_anonymous_controllers` is
# disabled, it creates a subclass of `ApplicationController`
end
end
ApplicationController
中指定的错误# spec/controllers/application_controller_spec.rb
require "rails_helper"
class ApplicationController < ActionController::Base
class AccessDenied < StandardError; end
rescue_from AccessDenied, :with => :access_denied
private
def access_denied
redirect_to "/401.html"
end
end
RSpec.describe ApplicationController, :type => :controller do
controller do
def index
raise ApplicationController::AccessDenied
end
end
describe "handling AccessDenied exceptions" do
it "redirects to the /401.html page" do
get :index
expect(response).to redirect_to("/401.html")
end
end
end
运行测试
$ rspec spec
all 通过测试
ApplicationController
中指定的错误# spec/controllers/application_controller_spec.rb
require "rails_helper"
class ApplicationController < ActionController::Base
class AccessDenied < StandardError; end
rescue_from AccessDenied, :with => :access_denied
private
def access_denied
render "errors/401"
end
end
RSpec.describe ApplicationController, :type => :controller do
controller do
def index
raise ApplicationController::AccessDenied
end
end
describe "handling AccessDenied exceptions" do
it "renders the errors/401 template" do
get :index
expect(response).to render_template("errors/401")
end
end
end
运行测试
$ rspec spec
all 通过测试
ApplicationController
中指定的错误# spec/controllers/application_controller_spec.rb
require "rails_helper"
class ApplicationController < ActionController::Base
class AccessDenied < StandardError; end
rescue_from AccessDenied, :with => :access_denied
private
def access_denied
render :file => "errors/401"
end
end
RSpec.describe ApplicationController, :type => :controller do
controller do
def index
raise ApplicationController::AccessDenied
end
end
describe "handling AccessDenied exceptions" do
it "renders the errors/401 template" do
get :index
expect(response).to render_template("errors/401")
end
end
end
运行测试
$ rspec spec
all 通过测试
ApplicationController
中指定的错误# spec/controllers/application_controller_subclass_spec.rb
require "rails_helper"
class ApplicationController < ActionController::Base
class AccessDenied < StandardError; end
end
class FoosController < ApplicationController
rescue_from ApplicationController::AccessDenied, with: :access_denied
private
def access_denied
redirect_to "/401.html"
end
end
RSpec.describe FoosController, :type => :controller do
controller(FoosController) do
def index
raise ApplicationController::AccessDenied
end
end
describe "handling AccessDenied exceptions" do
it "redirects to the /401.html page" do
get :index
expect(response).to redirect_to("/401.html")
end
end
end
运行测试
$ rspec spec
all 通过测试
# spec/controllers/base_class_can_be_inferred_spec.rb
require "rails_helper"
class ApplicationController < ActionController::Base; end
class FoosController < ApplicationController; end
RSpec.describe FoosController, :type => :controller do
controller do
def index
render :plain => "Hello World"
end
end
it "creates anonymous controller derived from FoosController" do
expect(controller).to be_a_kind_of(FoosController)
end
end
运行测试
$ rspec spec
all 通过测试
name
和controller_name
# spec/controllers/get_name_and_controller_name_from_described_class_spec.rb
require "rails_helper"
class ApplicationController < ActionController::Base; end
class FoosController < ApplicationController; end
RSpec.describe "Access controller names", :type => :controller do
controller FoosController do
def index
@name = self.class.name
@controller_name = controller_name
render :plain => "Hello World"
end
end
before do
get :index
end
it "gets the class name as described" do
expect(assigns[:name]).to eq('FoosController')
end
it "gets the controller_name as described" do
expect(assigns[:controller_name]).to eq('foos')
end
end
运行测试
$ rspec spec
all 通过测试
around_filter
和around_action
# spec/controllers/application_controller_around_filter_spec.rb
require "rails_helper"
class ApplicationController < ActionController::Base
around_action :an_around_filter
def an_around_filter
@callback_invoked = true
yield
end
end
RSpec.describe ApplicationController, :type => :controller do
controller do
def index
render :plain => ""
end
end
it "invokes the callback" do
get :index
expect(assigns[:callback_invoked]).to be_truthy
end
end
运行测试
$ rspec spec
all 通过测试
# spec/controllers/application_controller_spec.rb
require "rails_helper"
if defined?(ActionController::UrlGenerationError)
ExpectedRoutingError = ActionController::UrlGenerationError
else
ExpectedRoutingError = ActionController::RoutingError
end
RSpec.describe ApplicationController, :type => :controller do
controller do
def index
render :plain => "index called"
end
def create
render :plain => "create called"
end
def new
render :plain => "new called"
end
def show
render :plain => "show called"
end
def edit
render :plain => "edit called"
end
def update
render :plain => "update called"
end
def destroy
render :plain => "destroy called"
end
def willerror
render :plain => "will not render"
end
end
describe "#index" do
it "responds to GET" do
get :index
expect(response.body).to eq "index called"
end
it "also responds to POST" do
post :index
expect(response.body).to eq "index called"
end
it "also responds to PUT" do
put :index
expect(response.body).to eq "index called"
end
it "also responds to DELETE" do
delete :index
expect(response.body).to eq "index called"
end
end
describe "#create" do
it "responds to POST" do
post :create
expect(response.body).to eq "create called"
end
# And the rest...
%w{get post put delete}.each do |calltype|
it "responds to #{calltype}" do
send(calltype, :create)
expect(response.body).to eq "create called"
end
end
end
describe "#new" do
it "responds to GET" do
get :new
expect(response.body).to eq "new called"
end
# And the rest...
%w{get post put delete}.each do |calltype|
it "responds to #{calltype}" do
send(calltype, :new)
expect(response.body).to eq "new called"
end
end
end
describe "#edit" do
it "responds to GET" do
get :edit, :params => { :id => "anyid" }
expect(response.body).to eq "edit called"
end
it "requires the :id parameter" do
expect { get :edit }.to raise_error(ExpectedRoutingError)
end
# And the rest...
%w{get post put delete}.each do |calltype|
it "responds to #{calltype}" do
send(calltype, :edit, :params => {:id => "anyid"})
expect(response.body).to eq "edit called"
end
end
end
describe "#show" do
it "responds to GET" do
get :show, :params => { :id => "anyid" }
expect(response.body).to eq "show called"
end
it "requires the :id parameter" do
expect { get :show }.to raise_error(ExpectedRoutingError)
end
# And the rest...
%w{get post put delete}.each do |calltype|
it "responds to #{calltype}" do
send(calltype, :show, :params => {:id => "anyid"})
expect(response.body).to eq "show called"
end
end
end
describe "#update" do
it "responds to PUT" do
put :update, :params => { :id => "anyid" }
expect(response.body).to eq "update called"
end
it "requires the :id parameter" do
expect { put :update }.to raise_error(ExpectedRoutingError)
end
# And the rest...
%w{get post put delete}.each do |calltype|
it "responds to #{calltype}" do
send(calltype, :update, :params => {:id => "anyid"})
expect(response.body).to eq "update called"
end
end
end
describe "#destroy" do
it "responds to DELETE" do
delete :destroy, :params => { :id => "anyid" }
expect(response.body).to eq "destroy called"
end
it "requires the :id parameter" do
expect { delete :destroy }.to raise_error(ExpectedRoutingError)
end
# And the rest...
%w{get post put delete}.each do |calltype|
it "responds to #{calltype}" do
send(calltype, :destroy, :params => {:id => "anyid"})
expect(response.body).to eq "destroy called"
end
end
end
describe "#willerror" do
it "cannot be called" do
expect { get :willerror }.to raise_error(ExpectedRoutingError)
end
end
end
运行测试
$ rspec spec
all 通过测试
# spec/controllers/other_controller_spec.rb
require "rails_helper"
class OtherController < ActionController::Base
end
RSpec.describe OtherController, :type => :controller do
controller do
def custom
render :plain => "custom called"
end
end
specify "manually draw the route to request a custom action" do
routes.draw { get "custom" => "other#custom" }
get :custom
expect(response.body).to eq "custom called"
end
end
运行测试
$ rspec spec
all 通过测试
# spec/controllers/application_controller_spec.rb
require "rails_helper"
class FoosController < ApplicationController; end
RSpec.describe ApplicationController, :type => :controller do
controller FoosController do
def custom
render :plain => "custom called"
end
end
specify "manually draw the route to request a custom action" do
routes.draw { get "custom" => "foos#custom" }
get :custom
expect(response.body).to eq "custom called"
end
end
运行测试
$ rspec spec
all 通过测试
# spec/controllers/namespaced_controller_spec.rb
require "rails_helper"
class ApplicationController < ActionController::Base; end
module Outer
module Inner
class FoosController < ApplicationController; end
end
end
RSpec.describe Outer::Inner::FoosController, :type => :controller do
controller do
def index
@name = self.class.name
@controller_name = controller_name
render :plain => "Hello World"
end
end
it "creates anonymous controller derived from the namespace" do
expect(controller).to be_a_kind_of(Outer::Inner::FoosController)
end
it "gets the class name as described" do
expect{ get :index }.to change{
assigns[:name]
}.to eq('Outer::Inner::FoosController')
end
it "gets the controller_name as described" do
expect{ get :index }.to change{
assigns[:controller_name]
}.to eq('foos')
end
end
运行测试
$ rspec spec
all 通过测试
# spec/controllers/application_controller_spec.rb
require "rails_helper"
Rails.application.routes.draw do
match "/login" => "sessions#new", :as => "login", :via => "get"
end
RSpec.describe ApplicationController, :type => :controller do
controller do
def index
redirect_to login_url
end
end
it "redirects to the login page" do
get :index
expect(response).to redirect_to("/login")
end
end
运行测试
$ rspec spec
all 通过测试
使用bypass_rescue
可以绕过两个 Rails 对错误的默认处理控制器动作,以及使用 screw_from 声明的任何自定义处理。
这使您可以指定引发异常的详细信息。
# spec/controllers/gadgets_controller_spec_context.rb
class AccessDenied < StandardError; end
class ApplicationController < ActionController::Base
rescue_from AccessDenied, :with => :access_denied
private
def access_denied
redirect_to "/401.html"
end
end
rescue_from
处理标准异常# spec/controllers/gadgets_controller_spec.rb
require "rails_helper"
require 'controllers/gadgets_controller_spec_context'
RSpec.describe GadgetsController, :type => :controller do
before do
def controller.index
raise AccessDenied
end
end
describe "index" do
it "redirects to the /401.html page" do
get :index
expect(response).to redirect_to("/401.html")
end
end
end
运行测试
$ rspec spec/controllers/gadgets_controller_spec.rb
all 通过测试
bypass_rescue
绕过rescue_from
处理# spec/controllers/gadgets_controller_spec.rb
require "rails_helper"
require 'controllers/gadgets_controller_spec_context'
RSpec.describe GadgetsController, :type => :controller do
before do
def controller.index
raise AccessDenied
end
end
describe "index" do
it "raises AccessDenied" do
bypass_rescue
# 断言抛出异常
expect { get :index }.to raise_error(AccessDenied)
end
end
end
运行测试
$ rspec spec/controllers/gadgets_controller_spec.rb
all 通过测试
# spec/controllers/widgets_controller_spec.rb
require "rails_helper"
# A very simple Rails engine
module MyEngine
class Engine < ::Rails::Engine
isolate_namespace MyEngine
end
Engine.routes.draw do
resources :widgets, :only => [:show] do
get :random, :on => :collection
end
end
class WidgetsController < ::ActionController::Base
def random
@random_widget = Widget.all.shuffle.first
redirect_to widget_path(@random_widget)
end
def show
@widget = Widget.find(params[:id])
render :text => @widget.name
end
end
end
RSpec.describe MyEngine::WidgetsController, :type => :controller do
routes { MyEngine::Engine.routes }
it "redirects to a random widget" do
widget1 = Widget.create!(:name => "Widget 1")
widget2 = Widget.create!(:name => "Widget 2")
get :random
expect(response).to be_redirect
expect(response).to redirect_to(assigns(:random_widget))
end
end
运行测试
$ rspec spec
all 通过测试
# spec/controllers/application_controller_spec.rb
require "rails_helper"
RSpec.describe ApplicationController, :type => :controller do
controller do
def clear_cookie
cookies.delete(:user_name)
head :ok
end
end
before do
routes.draw { get "clear_cookie" => "anonymous#clear_cookie" }
end
it "clear cookie's value 'user_name'" do
cookies[:user_name] = "Sam"
get :clear_cookie
expect(cookies[:user_name]).to eq nil
end
end
运行测试
$ rspec spec
all 通过测试
不推荐直接在控制器中设置请求头,如果非要设置,请使用下面的 request.headers
方法
# spec/controllers/application_controller_spec.rb
require "rails_helper"
RSpec.describe ApplicationController, type: :controller do
controller do
def show
if request.headers["Authorization"] == "foo"
head :ok
else
head :forbidden
end
end
end
before do
routes.draw { get "show" => "anonymous#show" }
end
context "valid Authorization header" do
it "returns a 200" do
request.headers["Authorization"] = "foo"
get :show
expect(response).to have_http_status(:ok)
end
end
context "invalid Authorization header" do
it "returns a 403" do
request.headers["Authorization"] = "bar"
get :show
expect(response).to have_http_status(:forbidden)
end
end
end
运行测试
$ rspec spec
all 通过测试
rspec-rails 提供了许多自定义匹配器,其中大多数是 rspec 的用于 Rails 的断言装饰器。所以完全兼容 Rails 默认的断言方式。
关于兼容性,例如:
# rails 写法
assert_equal 403, response.status
assert_response 403
# rspec写法
expect(response).to have_http_status(403)
完全等价
# spec/models/widget_spec.rb
require "rails_helper"
RSpec.describe Widget do
context "when initialized" do
subject(:widget) { Widget.new }
it "is a new widget" do
expect(widget).to be_a_new(Widget)
end
it "is not a new string" do
expect(widget).not_to be_a_new(String)
end
end
context "when saved" do
subject(:widget) { Widget.create }
it "is not a new widget" do
expect(widget).not_to be_a_new(Widget)
end
it "is not a new string" do
expect(widget).not_to be_a_new(String)
end
end
end
运行测试
$ rspec spec/models/widget_spec.rb
all 通过测试
# spec/controllers/gadgets_spec.rb
require "rails_helper"
RSpec.describe GadgetsController do
describe "GET #index" do
subject { get :index }
it "renders the index template" do
expect(subject).to render_template(:index)
expect(subject).to render_template("index")
expect(subject).to render_template("gadgets/index")
end
it "does not render a different template" do
expect(subject).to_not render_template("gadgets/show")
end
end
end
运行测试
$ rspec spec/controllers/gadgets_spec.rb
all 通过测试
# spec/controllers/gadgets_spec.rb
require "rails_helper"
RSpec.describe GadgetsController do
describe "GET #index" do
subject { get :index }
it "renders the application layout" do
expect(subject).to render_template("layouts/application")
end
it "does not render a different layout" do
expect(subject).to_not render_template("layouts/admin")
end
end
end
运行测试
$ rspec spec/controllers/gadgets_spec.rb
all 通过测试
# spec/views/gadgets/index.html.erb_spec.rb
require "rails_helper"
RSpec.describe "gadgets/index" do
it "renders the index template" do
assign(:gadgets, [Gadget.create!])
render
expect(view).to render_template(:index)
expect(view).to render_template("index")
expect(view).to render_template("gadgets/index")
end
it "does not render a different template" do
expect(view).to_not render_template("gadgets/show")
end
end
运行测试
$ rspec spec/views
all 通过测试
# spec/controllers/widgets_controller_spec.rb
require "rails_helper"
RSpec.describe WidgetsController do
describe "#create" do
subject { post :create, :params => { :widget => { :name => "Foo" } } }
it "redirects to widget_url(@widget)" do
expect(subject).to redirect_to(widget_url(assigns(:widget)))
end
it "redirects_to :action => :show" do
expect(subject).to redirect_to :action => :show,
:id => assigns(:widget).id
end
it "redirects_to(@widget)" do
expect(subject).to redirect_to(assigns(:widget))
end
it "redirects_to /widgets/:id" do
expect(subject).to redirect_to("/widgets/#{assigns(:widget).id}")
end
end
end
运行测试
$ rspec spec/controllers/widgets_controller_spec.rb
all 通过测试
# spec/controllers/application_controller_spec.rb
require "rails_helper"
RSpec.describe ApplicationController, :type => :controller do
controller do
def index
render :json => {}, :status => 209
end
end
describe "GET #index" do
it "returns a 209 custom status code" do
get :index
expect(response).to have_http_status(209)
end
end
end
运行测试
$ rspec spec
all 通过测试
# spec/controllers/application_controller_spec.rb
require "rails_helper"
RSpec.describe ApplicationController, :type => :controller do
controller do
def index
render :json => {}, :status => :see_other
end
end
describe "GET #index" do
it "returns a :see_other status code" do
get :index
expect(response).to have_http_status(:see_other)
end
end
end
运行测试
$ rspec spec
all 通过测试
# spec/controllers/application_controller_spec.rb
require "rails_helper"
RSpec.describe ApplicationController, :type => :controller do
controller do
def index
render :json => {}, :status => :bad_gateway
end
end
describe "GET #index" do
it "returns a some type of error status code" do
get :index
expect(response).to have_http_status(:error)
end
end
end
运行测试
$ rspec spec
all 通过测试
# spec/controllers/gadgets_spec.rb
require "rails_helper"
RSpec.describe GadgetsController, :type => :controller do
describe "GET #index" do
it "returns a 200 OK status" do
get :index
expect(response).to have_http_status(:ok)
end
end
end
运行测试
$ rspec spec/controllers/gadgets_spec.rb
all 通过测试
# spec/requests/gadgets/widget_management_spec.rb
require "rails_helper"
RSpec.describe "Widget management", :type => :request do
it "creates a Widget and redirects to the Widget's page" do
get "/widgets/new"
expect(response).to have_http_status(:ok)
post "/widgets", :params => { :widget => {:name => "My Widget"} }
expect(response).to have_http_status(302)
follow_redirect!
expect(response).to have_http_status(:success)
end
end
运行测试
$ rspec spec/requests
all 通过测试
# spec/features/widget_management_spec.rb
require "rails_helper"
RSpec.feature "Widget management", :type => :feature do
scenario "User creates a new widget" do
visit "/widgets/new"
expect(page).to have_http_status(200)
click_button "Create Widget"
expect(page).to have_http_status(:success)
end
end
运行测试
$ rspec spec/features/widget_management_spec.rb
all 通过测试
# spec/models/widget_spec.rb
require "rails_helper"
RSpec.describe Widget do
let!(:widgets) { Array.new(3) { Widget.create } }
subject { Widget.all }
it "returns all widgets in any order" do
expect(subject).to match_array(widgets)
end
end
运行测试
$ rspec spec/models/widget_spec.rb
all 通过测试
检查指定的任务是否在排队
# spec/jobs/upload_backups_job_spec.rb
require "rails_helper"
RSpec.describe UploadBackupsJob do
it "matches with enqueued job" do
ActiveJob::Base.queue_adapter = :test
UploadBackupsJob.perform_later
expect(UploadBackupsJob).to have_been_enqueued
end
end
运行测试
$ rspec spec/jobs/upload_backups_job_spec.rb
all 通过测试
# spec/jobs/upload_backups_job_spec.rb
require "rails_helper"
RSpec.describe UploadBackupsJob do
it "matches with enqueued job" do
ActiveJob::Base.queue_adapter = :test
UploadBackupsJob.perform_later("users-backup.txt", "products-backup.txt")
expect(UploadBackupsJob).to(
have_been_enqueued.with("users-backup.txt", "products-backup.txt")
)
end
end
运行测试
$ rspec spec/jobs/upload_backups_job_spec.rb
all 通过测试
# spec/jobs/upload_backups_job_spec.rb"
require "rails_helper"
RSpec.describe UploadBackupsJob do
it "matches with enqueued job" do
ActiveJob::Base.queue_adapter = :test
UploadBackupsJob.set(:wait_until => Date.tomorrow.noon).perform_later
expect(UploadBackupsJob).to have_been_enqueued.at(Date.tomorrow.noon)
end
end
运行测试
$ rspec spec/jobs/upload_backups_job_spec.rb
all 通过测试
# spec/jobs/upload_backups_job_spec.rb
require "rails_helper"
RSpec.describe UploadBackupsJob do
it "matches with enqueued job" do
ActiveJob::Base.queue_adapter = :test
UploadBackupsJob.perform_later
expect(UploadBackupsJob).to have_been_enqueued.at(:no_wait)
end
end
运行测试
$ rspec spec/jobs/upload_backups_job_spec.rb
all 通过测试
# spec/jobs/upload_backups_job_spec.rb" with:
require "rails_helper"
RSpec.describe UploadBackupsJob do
it "matches with enqueued job" do
ActiveJob::Base.queue_adapter = :test
UploadBackupsJob.perform_later
expect(UploadBackupsJob).to have_been_enqueued.on_queue("default")
end
end
运行测试
$ rspec spec/jobs/upload_backups_job_spec.rb
all 通过测试
检查任务是否已经执行
# spec/jobs/upload_backups_job_spec.rb
require "rails_helper"
RSpec.describe UploadBackupsJob do
it "matches with performed job" do
ActiveJob::Base.queue_adapter = :test
ActiveJob::Base.queue_adapter.perform_enqueued_jobs = true
UploadBackupsJob.perform_later
expect(UploadBackupsJob).to have_been_performed
end
end
运行测试
$ rspec spec/jobs/upload_backups_job_spec.rb
all 通过测试
# spec/jobs/upload_backups_job_spec.rb
require "rails_helper"
RSpec.describe UploadBackupsJob do
it "matches with performed job" do
ActiveJob::Base.queue_adapter = :test
ActiveJob::Base.queue_adapter.perform_enqueued_jobs = true
UploadBackupsJob.perform_later("users-backup.txt", "products-backup.txt")
expect(UploadBackupsJob).to(
have_been_performed.with("users-backup.txt", "products-backup.txt")
)
end
end
运行测试
$ rspec spec/jobs/upload_backups_job_spec.rb
all 通过测试
# spec/jobs/upload_backups_job_spec.rb
require "rails_helper"
RSpec.describe UploadBackupsJob do
it "matches with performed job" do
ActiveJob::Base.queue_adapter = :test
ActiveJob::Base.queue_adapter.perform_enqueued_jobs = true
ActiveJob::Base.queue_adapter.perform_enqueued_at_jobs = true
UploadBackupsJob.set(:wait_until => Date.tomorrow.noon).perform_later
expect(UploadBackupsJob).to have_been_performed.at(Date.tomorrow.noon)
end
end
运行测试
$ rspec spec/jobs/upload_backups_job_spec.rb
all 通过测试
# spec/jobs/upload_backups_job_spec.rb
require "rails_helper"
RSpec.describe UploadBackupsJob do
it "matches with performed job" do
ActiveJob::Base.queue_adapter = :test
ActiveJob::Base.queue_adapter.perform_enqueued_jobs = true
UploadBackupsJob.perform_later
expect(UploadBackupsJob).to have_been_performed.on_queue("default")
end
end
运行测试
$ rspec spec/jobs/upload_backups_job_spec.rb
all 通过测试
# spec/requests/widget_management_spec.rb
require "rails_helper"
RSpec.describe "Widget management", :type => :request do
it "creates a Widget and redirects to the Widget's page" do
get "/widgets/new"
expect(response).to render_template(:new)
post "/widgets", :params => { :widget => {:name => "My Widget"} }
expect(response).to redirect_to(assigns(:widget))
follow_redirect!
expect(response).to render_template(:show)
expect(response.body).to include("Widget was successfully created.")
end
it "does not render a different template" do
get "/widgets/new"
expect(response).to_not render_template(:show)
end
end
运行测试
$ rspec spec/requests/widget_management_spec.rb
all 通过测试
# spec/requests/widget_management_spec.rb
require "rails_helper"
RSpec.describe "Widget management", :type => :request do
it "creates a Widget" do
# 设置请求头
headers = { "ACCEPT" => "application/json" }
# 设置响应数据格式
post "/widgets", :params => { :widget => {:name => "My Widget"} }, :headers => headers
expect(response.content_type).to eq("application/json")
expect(response).to have_http_status(:created)
end
end
运行测试
$ rspec spec/requests/widget_management_spec.rb
all 通过测试
# spec/requests/widget_management_spec.rb
require "rails_helper"
RSpec.describe "Widget management", :type => :request do
it "creates a Widget" do
headers = { "ACCEPT" => "application/json" }
post "/widgets", :params => { :widget => {:name => "My Widget"} }, :headers => headers
expect(response.content_type).to eq("application/json; charset=utf-8")
expect(response).to have_http_status(:created)
end
end
运行测试
$ rspec spec/requests/widget_management_spec.rb
all 通过测试
# spec/requests/widget_management_spec.rb
require "rails_helper"
RSpec.describe "Widget management", :type => :request do
it "creates a Widget and redirects to the Widget's page" do
# 设置请求数据的格式
headers = { "CONTENT_TYPE" => "application/json" }
post "/widgets", :params => '{ "widget": { "name":"My Widget" } }', :headers => headers
expect(response).to redirect_to(assigns(:widget))
end
end
运行测试
$ rspec spec/requests/widget_management_spec.rb
all 通过测试
# spec/requests/widgets_spec.rb
require "rails_helper"
# 简单的 Rails engine
module MyEngine
class Engine < ::Rails::Engine
isolate_namespace MyEngine
end
class LinksController < ::ActionController::Base
def index
render plain: 'hit_engine_route'
end
end
end
MyEngine::Engine.routes.draw do
resources :links, :only => [:index]
end
Rails.application.routes.draw do
mount MyEngine::Engine => "/my_engine"
end
module MyEngine
RSpec.describe "Links", :type => :request do
include Engine.routes.url_helpers
it "redirects to a random widget" do
# 允许使用 辅助方法访问
get links_path
expect(response.body).to eq('hit_engine_route')
end
end
end
运行测试
$ rspec spec
all 通过测试
# config/initializers/mailer_defaults.rb
Rails.configuration.action_mailer.default_url_options = { :host => 'example.com' }
# spec/mailers/notifications_spec.rb
require 'rails_helper'
RSpec.describe NotificationsMailer, :type => :mailer do
it 'should have access to URL helpers' do
expect { gadgets_url }.not_to raise_error
end
end
运行测试
$ rspec spec
all 通过测试
# config/initializers/mailer_defaults.rb
# no default options
#spec/mailers/notifications_spec.rb
require 'rails_helper'
RSpec.describe NotificationsMailer, :type => :mailer do
it 'should have access to URL helpers' do
expect { gadgets_url :host => 'example.com' }.not_to raise_error
expect { gadgets_url }.to raise_error
end
end
运行测试
$ rspec spec
all 通过测试
# spec/mailers/notifications_mailer_spec.rb" with:
require "rails_helper"
# 最外层的架构快
# RSpec.describe 类名, :type => :类型 do
RSpec.describe NotificationsMailer, :type => :mailer do
# 一个 describe 块
describe "notify" do
let(:mail) { NotificationsMailer.signup }
# 一个测试用例
it "renders the headers" do
# expect(值).to eq(等于的值)
expect(mail.subject).to eq("Signup")
expect(mail.to).to eq(["[email protected]"])
expect(mail.from).to eq(["[email protected]"])
end
# 一个测试用例
it "renders the body" do
expect(mail.body.encoded).to match("Hi")
end
end
end
运行测试
$ rspec spec
all 通过测试
Rails 5 新增 file fixture 对象。
稳进默认存储路径:spec/fixtures/files
File fixtures 代表了 +Pathname+ 对象,使用很简单。
实例:
file_fixture("example.txt").read # get the file's content
file_fixture("example.mp3").size # get the file size
通过配置可以改变默认文件位置
RSpec.configure do |config|
config.file_fixture_path = "spec/custom_directory"
end
# spec/fixtures/files/sample.txt
Hello
# spec/lib/file_spec.rb
require "rails_helper"
RSpec.describe "file" do
it "reads sample file" do
#期待.读取文件的内容.为 等于("hello")
expect(file_fixture("sample.txt").read).to eq("Hello")
end
end
运行测试
$ rspec spec/lib/file_spec.rb
all 通过测试