Rails 理解 Rails 5 中 Controller 和 Integration 测试

grantbb · 2016年08月07日 · 最后由 zw963 回复于 2016年08月25日 · 7964 次阅读
本帖已被管理员设置为精华贴

这篇文章首先介绍 Rails5 中 controller 测试的变化,然后通过类图来分析 Rails5 中的IntegrationTest相关的类组织结构。通过理解相关的类和模块的关系来帮助我们写出更好的测试。

在之前的文章minitest + capybara 测试基于 devise 的用户注册中,也是通过创建继承自ActionDispatch::IntegrationTestFeatureTest类,然后引入 Capybara 的 DSL 模块,来方便我们创建其他 Feature Test 或者叫 User Acceptance Test。

第一部分:Rails5 中 controller 测试的变化

1. ActionController::TestCase 废弃掉了

在 Rails5 中 Controller 测试都是继承自ActionDispatch::IntegrationTest类,而不是之前的ActionController::TestCase。如果还想继续使用,那么可以使用这个 gem: rails-controller-testing

2. assigns 和 assert_template 也废弃掉了

在 Rails4 的 controller 测试中assigns方法用于获得 action 中的实例变量然后进行验证,assert_template用于验证 action 最后渲染了指定的 template。

在 Rails5 中,controller 测试强调的更多的是 action 的处理结果,比如响应的状态和响应的结果。

如果你还是想使用上面这两个方法,还是可以在rails-controller-testing这个 gem 中找到他们。

3. 移走 assert_select 等方法

assert_select等验证响应的 HTML 内容的方法,已经移到单独的rails-dom-testing这个 gem 中。

所以,如果是我要验证页面的内容和样式等,还是通过 capybara 来进行精确的操作和验证。

4. 使用 URL 而不是 Action 来发送请求

在 Rails4 中,通过 action 的名字来发送请求(说实话我一直很不习惯)

class PicturesControllerTest < ActionController::TestCase

  def test_index_response
    get :index
    assert_response :success
  end
end

而在 Rails5 中,要换成 URL(多直观),否则就会抛出异常:URI::InvalidURIError: bad URI

class PicturesControllerTest < ActionDispatch::IntegrationTest

  def test_index
    get pictures_url
    assert_response :success
  end
end

5. HTTP 的请求方法中必须使用关键字参数

在 Rails5 中,HTTP 请求的方法参数必须明确指定关键字,比如 params,flash 等。这样会让代码更加清楚。请看例子:

class PicturesControllerTest < ActionDispatch::IntegrationTest

  def test_create
    post picture_url, params: { picture: { name: "sea" } }
    assert_response :success
  end
end

第二部分:理解 ActionDispatch::IntegrationTest 类和相关 module

在继承了ActionDispatch::IntegrationTest类的 Controller 测试中,我们可以使用很多方便的 helper 方法和大量用于结果验证的 assertions 方法。 比如跟响应相关的:

json = response.parsed_body # 解析json格式的响应结果
assert_response :success # 验证成功的请求

还有跟路由 routing 相关的

assert_routing({ method: 'post', path: '/pictures' }, controller: 'pictures', action: 'create')
assert_recognizes({ controller: 'pictures', action: 'index' }, '/')

如果你要测试文件上传功能,Rails 提供了非常方便的方法fixture_file_upload。但是你会发现你无法在 controller 中直接使用,你需要引入ActionDispatch::TestProcess模块。有点奇怪?

所以,为了搞清楚这些 helper 方法和 assertions 的来源,也方便我们日后查询相关的文档,我会通过下面的类图来理解 IntegrationTest 这个类。

1. ActiveSupport::TestCase

首先在 Rails 中我们有ActiveSupport::TestCase类,它继承自Minitest::Test类,然后像ActionDispatch::IntegrationTest, ActionView::TestCase, ActiveJob::TestCase等我们自己的测试需要继承的测试基类,都继承自ActiveSupport::TestCase类。同时你会发现,我们自己的 model 的测试都是直接继承自ActiveSupport::TestCase类。

所以在我们的测试中,可以直接使用 minitest 提供的一些 assertions,比如常见的:

assert_equal( expected, actual, [msg] )
assert_includes( collection, obj, [msg] )
assert_instance_of( class, obj, [msg] )

由于ActiveSupport::TestCase引入了ActiveSupport::Testing::Assertions模块,所以我们可以使用非常方便的方法

assert_difference(expression, difference = 1, message = nil, &block)
# 例如
assert_difference 'Article.count' do
  post :create, params: { article: {...} }
end

assert_no_difference(expression, message = nil, &block)

另外还有两个比较有用的被引入的模块是ActiveSupport::Testing::FileFixturesActiveSupport::Testing::TimeHelpers。他们分别提供了访问 fixtures 下面的文件和修改测试时间的方法

file_fixture(fixture_name)

travel(duration, &block)
travel_back()
travel_to(date_or_time)

2. ActionDispatch::IntegrationTest

接下来我们再来看看IntegrationTest这个类,它首先通过引入Integration::Runner模块,从而一起引入了ActionDispatch::Assertions模块,然后 Runner 中运行测试的时候,会创建Integration::Session类的实例,Integration::Session引入了Integration::RequestHelpers模块,所以我们就可以使用像 get, post, put 等 HTTP 请求相关的方法。 具体的这些请求相关的方法,参考文档: ActionDispatch::Integration::RequestHelpers

了解了发送请求的方法后,我们再来看看ActionDispatch::Assertions模块,它只是引入了另外两个重要的 module:ActionDispatch::Assertions::ResponseAssertionsActionDispatch::Assertions::RoutingAssertions

ResponseAssertions中提供了常用的 assert_redirected_toassert_response方法

assert_redirected_to login_url
assert_response :redirect

RoutingAssertions中提供了上面展示过的:assert_generatesassert_recognizesassert_routing方法,用于进行路由 Routing 相关的测试。

所以,理解了IntegrationTest的结构后,我们就知道为什么我们还需要引入ActionDispatch::TestProcess来测试文件上传,详细的如何测试 Carrierwave 的文件上传功能我会在下一篇文章中介绍。

有分量的帖子 👍🏻

谢谢点赞,希望越来越多人使用 minitest + rails

lgn21st 将本帖设为了精华贴。 08月15日 17:44

好贴,非常认真细致的总结,加精支持!

controller 测试还是有必要的,虽然现在被抽出来了,但我个人肯定是要自己加回来的(还好 rspec-raills 没学坏)。比方说测试 application 级别的 filter,更合适利用 controller 测试来做,别忘了 ActionController::TestCase 有个叫做 controller 的方法可以让你在测试代码中模拟派生控制器:

RSpec.describe Api::ApplicationController, type: :controller do
  controller do
    skip_before_action :authenticate, only: [:new]

    def index;end
    def new;end
  end

  #......

  context '未提供Token,认证失败' do
    before do
      get :index, format: 'json'
    end

    it '返回401状态码' do
      expect(response).to have_http_status 401
    end

    it '返回正确的响应头' do
      expect(response.header).to have_key 'WWW-Authenticate'
    end
  end

  #......
end

恩,你是可以针对某个具体的子类来测试,但我想说,如果严格按照 TDD 的流程来做,这个时候你是还没走到子类控制器那一步的好不好……

关于 Rails 测试,或许闲下来我会写个系列探讨下。

另外关于从 action 名称改成 url,那真是很蛋疼的事儿,写 users_path 是不是比:create 更浪费击键次数?很明显嘛……

#6 楼 @scriptfans 是否一定要 TDD,这个也是个仁者见仁智者见智的事情,大牛们已经有很好的讨论。Is TDD Dead?,这两天我也会再学习一下。同时推荐大家也看看。

针对#6 楼提出的测试 ApplicationController 中 filter 的测试,我给出一个测试 Devise 的:authenticate_user! 的例子

class ApplicationController < ActionController::Base
  protect_from_forgery with: :exception

  before_action :authenticate_user!
end

然后创建一个/test/controllers/base_controller_test.rb

require 'test_helper'

class BaseController < ApplicationController
  def index
    head :ok
  end
end

class BaseControllerTest  < ActionDispatch::IntegrationTest
  include Devise::Test::IntegrationHelpers

  setup do
    Rails.application.routes.draw do
      get 'base' => 'base#index'
    end
  end

  teardown do
    Rails.application.reload_routes!
  end

  test 'redirects if user is not logedin' do
    get '/base'

    assert_response :redirect
    assert_redirected_to 'http://www.example.com/'
  end

  test 'returns success if user is loggedin' do
    sign_in users(:one)

    get '/base'
    assert_response :success
  end
end

可以看到使用 minitest,代码更加直观,没有太多的 magic,直接定义一个临时的 controller 来测试验证登录的 filter

手写的英文很好看

travel_to(date_or_time) support &block argument too.

in fact, travel_to invoke travel internally, so, it can support block as a argument.

需要 登录 后方可回复, 如果你还没有账号请 注册新账号