新手问题 如何解决测试代码重复的问题?

cuterxy · 2015年03月04日 · 最后由 cuterxy 回复于 2015年03月05日 · 3024 次阅读

最近在写测试代码,发现很多都是重复的逻辑,但是又有一点点不同,比如是某个 user 或者某个测试的对象的不同。大家有什么好的办法能把测试代码写得 dry 一些吗?

  1. 提炼成函数放到test_helper.rb或别的地方去
  2. 活用fixtures

没有例子讨论不了。

我懂你的意思,比如 user 要测试 用户名、昵称名、密码、重复密码等等的时候,排列组合的可能性太多,有的又不得不测。

我的回答是没有,要不然怎么那么多人不写测试代码。我的建议是在关键的地方写测试代码,很简单的逻辑增删改查之类的,或者对业务影响不大的,可以适当省去。毕竟工作还要讲求一个效率。

#1 楼 @spacewander 你说的我也想过,但是好像没有可以参考的代码。看了一些开源的项目,发现还没有比较系统的做法。

#2 楼 @Rei 不好意思,我偷懒了。具体的例子可以看一下我在 OGX 社区 的代码 https://github.com/ogx-io/ogx-io-web 里面的例子(由于代码太长就只发链接了):

https://github.com/ogx-io/ogx-io-web/blob/master/spec/controllers/admin/elite/nodes_controller_spec.rb

在这个例子当中,判断成功的代码有很多地方都是这么两句话:

expect(response).to be_success
expect(request.flash[:error]).to be_blank

而且,在一个 describe 底下,两个 context 可能只是测试对象的类型不同,而测试的内容都是一样的,但测试的预期结果也会有所不同。如果能够把这些情况归纳成一种模板的话,我觉得是不是测试的代码就能够减少很多,而且看起来也更加清晰易懂呢?

不知道我说清楚问题了没有?

#3 楼 @MrPasserby 你说的也是一种情况。我的理解就是,不可能所有排列组合都列举一遍,所以有时候的确是需要偷点懒,只要关键逻辑走通就可以了,其他实现逻辑一样的只测一个例子就可以了。

#5 楼 @cuterxy 写到 after 行不行?

这两句太简单我觉得不用简化。

在 Test::Unit 可以这样提取常用断言:

def assert_foobar(options)
  do_something options
  yield # if use block
  assert condition_one
  assert condition_two
end

test "case" do
  assert_foobar(options) do
    # code here
  end
end

Rspec 好像叫 Custom Matchers 吧,我觉得没有 Test::Unit plain Ruby 语法直观。

#8 楼 @Rei 其实就相当于是 helper 吧。我感觉就干脆提取出来放到一个 helper 文件里面更方便。只是有个问题,还是上面提到的两句话:

expect(response).to be_success
expect(request.flash[:error]).to be_blank

如果写成一个函数:

def test_if_success
  expect(response).to be_success
  expect(request.flash[:error]).to be_blank
end

那么其中的 response 和 request 是否也要用参数传入呢?这样就不是很方便了。如果避免传参数,那写法是否就应该改为:

def test_if_success
  eval <<CODE_BLOCK
  expect(response).to be_success
  expect(request.flash[:error]).to be_blank
CODE_BLOCK
end

#9 楼 @cuterxy 如果是 Test::Unit 的话不用,这两个是同一作用域的实例方法。Rspec 我不熟悉。

大概看了一下,你这些重复的代码都是和权限有关的对吧。给你分享下我现在用的办法:

# spec/support/shared_examples_for_authorized.rb

require "rails_helper"

RSpec.shared_examples "authorized" do |request_proc|

  let(:pass) { [create(:user)] }
  let(:deny) { [nil] }
  let(:status) { :unauthorized }

  it "返回其他的 http status" do
    pass.each do |user|
      user ? controller.sign_in(user) : controller.sign_out
      instance_exec(&request_proc)
      expect(response).not_to have_http_status(status)
    end
  end

  it "返回指定的 http status" do
    deny.each do |user|
      user ? controller.sign_in(user) : controller.sign_out
      instance_exec(&request_proc)
      expect(response).to have_http_status(status)
    end
  end

end

# spec/controllers/comments_controller_spec.rb

RSpec.describe CommentsController, :type => :controller do

  #...

  describe "GET edit" do
    it_behaves_like "authorized", -> { get :edit, {id: create(:comment, author: current_user).to_param} }
    it_behaves_like "authorized", -> { get :edit, {id: create(:comment, author: current_user).to_param} } do
      let(:status)  { :forbidden }
      let(:pass)    { [current_user, create(:user, :admin)] }
      let(:deny)    { [create(:user)] }
    end

    it "assigns the requested comment as @comment" do
      comment = create(:comment, author: current_user)
      get :edit, {id: comment.to_param}, valid_session
      expect(assigns(:comment)).to eq(comment)
    end
  end

  #...

end

不知道你能不能看明白?

#11 楼 @lolychee 明白了。RSpec 有 shared_examplesshared_context 这两种工具来解决类似的问题,之前一直都没有留意到。我再继续研究一下。谢谢!

第一,不需要参数,request 和 response 都是全局可用,直接定到@request@response变量。

第二,你不需要在每个测试里面都包含 response success 的逻辑,有变化可能的情况写一下,做到有足够的信心就可以了。包含的东西那么多,你要什么都测那就不用写别的东西了。

我看了一下你的测试文件,每个 example 里面的期待都有好几个,并且常常不关联,看起来不够清楚直接。

#13 楼 @billy 嗯,知道了。其实我加的这些测试用例基本上都是为了呈现出当初设计的意图,可能还是有太多冗余的地方了,这个以后继续改进吧。谢谢!

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