之前写 rspec,写的不是很如意,遇到 controller 复杂的 response,甚至不如 kotlin 写的 junit 简洁。
require 'rails_helper'
RSpec.describe UsersController, type: :controller do
include_context 'shared context'
# index returns {success: 1, data: [{name: 'Jay', id: 1}, {name: 'Jone', id: 2}]}
it 'test index' do
get :index
res = JSON.parse(response.body).deep_symbolize_keys
expect(res[:success]).to eq 1
expect(res[:data].size).to eq 2
expect(res[:data].first.name).to eq 'Jay'
expect(res[:data].first.id).to eq 1
expect(res[:data].second.name).to eq 'Jone'
expect(res[:data].second.name).not_to eq 'Jay'
expect(res[:data].second.id).to eq 1
end
end
作为一个优雅的人,怎么能忍受呢,于是通过元编程改造:
require 'rails_helper'
RSpec.describe UsersController, type: :controller do
include_context 'shared context'
# index returns {success: 1, data: [{name: 'Jay', id: 1}, {name: 'Jone', id: 2}]}
it 'test index' do
get :index
expect_response do
ex success, eq(1)
ex data do
ex size, eq(2)
item 0 do # 也可以写成 `ex first do `
ex name, eq('Jay')
ex id, eq(1)
end
item 1 do
ex name, eq('Jone')
ex_not name, eq('Jay')
ex id, eq(2)
end
end
end
end
end
最后贴改造源代码
# shared_contest.rb
module Matchable
extend ActiveSupport::Concern
included do
attr_accessor :context
include RSpec::Matchers
end
def ex obj, matcher = nil, &blk
if blk
obj.instance_eval &blk
else
context.instance_eval do
expect(obj).to matcher
end
end
end
def ex_not obj, matcher
context.instance_eval do
expect(obj).not_to matcher
end
end
end
RSpec.shared_context 'shared context', shared_context: :metadata do
before do
# some mock, before action mock
end
def res
json = JSON.parse(response.body)
json.is_a?(Hash) ? to_recursive_ostruct(json) : json
end
def to_recursive_ostruct(hash)
obj = OpenStruct.new(hash.each_with_object({}) do |(key, val), memo|
memo[key] = if val.is_a?(Hash)
to_recursive_ostruct(val)
elsif val.is_a?(Array)
result = val.map { |x| to_recursive_ostruct x }
result.context = self
result
else
val
end
end)
obj.context = self
obj
end
def expect_response &blk
ex res, &blk
end
def ex obj, &blk
obj.instance_eval &blk if blk
end
OpenStruct.include Matchable
class Array
include Matchable
def item idx, &blk
self[idx].instance_eval &blk if blk
end
end
end