最近在写测试代码,发现很多都是重复的逻辑,但是又有一点点不同,比如是某个 user 或者某个测试的对象的不同。大家有什么好的办法能把测试代码写得 dry 一些吗?
我懂你的意思,比如 user 要测试 用户名、昵称名、密码、重复密码等等的时候,排列组合的可能性太多,有的又不得不测。
我的回答是没有,要不然怎么那么多人不写测试代码。我的建议是在关键的地方写测试代码,很简单的逻辑增删改查之类的,或者对业务影响不大的,可以适当省去。毕竟工作还要讲求一个效率。
#2 楼 @Rei 不好意思,我偷懒了。具体的例子可以看一下我在 OGX 社区 的代码 https://github.com/ogx-io/ogx-io-web 里面的例子(由于代码太长就只发链接了):
在这个例子当中,判断成功的代码有很多地方都是这么两句话:
expect(response).to be_success
expect(request.flash[:error]).to be_blank
而且,在一个 describe 底下,两个 context 可能只是测试对象的类型不同,而测试的内容都是一样的,但测试的预期结果也会有所不同。如果能够把这些情况归纳成一种模板的话,我觉得是不是测试的代码就能够减少很多,而且看起来也更加清晰易懂呢?
不知道我说清楚问题了没有?
#3 楼 @MrPasserby 你说的也是一种情况。我的理解就是,不可能所有排列组合都列举一遍,所以有时候的确是需要偷点懒,只要关键逻辑走通就可以了,其他实现逻辑一样的只测一个例子就可以了。
在 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
大概看了一下,你这些重复的代码都是和权限有关的对吧。给你分享下我现在用的办法:
# 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_examples 和 shared_context 这两种工具来解决类似的问题,之前一直都没有留意到。我再继续研究一下。谢谢!