Ruby 写了一些 rspec 的 DSL 之后,逐渐意识到元编程的强大

tinyfeng · 2021年04月14日 · 最后由 linlinda 回复于 2023年11月03日 · 664 次阅读

之前写 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

其它可读性不谈,你把 not_to 给漏掉了

spike76 回复

后面可以加上,不是问题

非 rspec 用户,俺来写后面那截 data 断言的话,会直接写成

data = res[:data]
expect([data.size, data.first.name, data.first.id, data.second.name, data.second.id]).
to eq([2,'Jay', 1, 'Jone', 1])

写成最开始那样也很 ok, 可读性杠杠滴。

后面新封装的 DSD(Domain Specfic Dialect, 俺生造的词:领域专属土话,就是外人不咋好读,不咋好理解的意思), 感觉这个例子里没看出来代码量少了多少...

最后,做了复杂的封装,老哥应该还要写段 rspec 测试一下自己的 rspec 封装... (😁 逃~)

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