BigBing 技术博客

理解Rails5中Controller和Integration测试

这篇文章首先介绍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的文件上传功能我会在下一篇文章中介绍。


补充,本文发到rubychina论坛后,有朋友问如何测试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.