Rails 理解 Rails 5 中 Controller 和 Integration 测试

grantbb · 2016年08月07日 · 最后由 zw963 回复于 2016年08月25日 · 5665 次阅读
本帖已被设为精华帖!

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

共收到 12 条回复

有分量的帖子 👍🏻

谢谢点赞,希望越来越多人使用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更浪费击键次数?很明显嘛……

#5楼 @lgn21st 谢谢版主!

#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.

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