如何在minitest中模拟一个块?

ege*_*and 4 ruby tdd unit-testing minitest

希望对MiniTest的人们来说是一个简单的问题。

我有一段代码,将在这里浓缩为一个示例:

class Foo
  def initialize(name)
    @sqs    = Aws::SQS::Client.new
    @id     = @sqs.create_queue( queue_name: name ).fetch(:queue_url)
    @poller = Aws::SQS::QueuePoller.new(@id)
  end
  def pick_first
    @poller.poll(idle_timeout: 60) do |message|
      process_msg(message) if some_condition(message)
    end
  end
Run Code Online (Sandbox Code Playgroud)

我该如何模拟/存根/其他方式,以便我可以提供一个message通行证以供测试,some_condition()并可能通过处理process_msg()

即我要测试@poller.poll(idle_timeout: 60) do |message|

我试图Aws::SQS::QueuePoller#new用模拟轮询器对Stub进行存根,但是它不会产生|message|仅返回它的消息。

这就是我,这是工作:

mockqueue = MiniTest::Mock.new

mocksqs = MiniTest::Mock.new
mocksqs.expect :create_queue, mockqueue, [Hash]

mockpoller = MiniTest::Mock.new                                                                                                                         
mockpoller.expect :poll, 'message', [{ idle_timeout: 60 }]

Aws::SQS::Client.stub :new, mocksqs do
  Aws::SQS::QueuePoller.stub :new, mockpoller do
    queue = Foo.new(opts)
    queue.pick_first
  end
end
Run Code Online (Sandbox Code Playgroud)

如果我在中收到一个变量#pick_first,那就是模拟将其放入的地方,而不是|message|

def pick_first
    receiver = @poller.poll(idle_timeout: 60) do |message|
      process_msg(message) if some_condition(message)
    end
    puts receiver # this shows my 'message' !!! WHYYYY??
  end
Run Code Online (Sandbox Code Playgroud)

ege*_*and 6

如果其他人有相同的问题,请回答我自己的问题。

我通过Twitter寻求帮助,MiniTest的作者Ryan Davis(又名github上的@zenspider / Twitter上的@the_zenspider)给出了快速解答,并邀请将问题提交给MiniTest github问题跟踪器。

我这样做了,得到了瑞安(Ryan)和皮特·希金斯(Pete Higgins)(github上的@phiggins)的好评,我在此全文进行了全文复制。谢谢你们俩的帮助!


@phiggins说:

怎么样呢?

class Foo   def initialize(name, opts={})
  @sqs    = Aws::SQS::Client.new
  @id     = @sqs.create_queue( queue_name: name ).fetch(:queue_url)
  @poller = opts.fetch(:poller) { Aws::SQS::QueuePoller.new(@id) }   end

  def pick_first
    @poller.poll(idle_timeout: 60) do |message|
    process_msg(message) if some_condition(message)
    end
  end
end

# later, in your tests
describe Foo do
  it "does the thing in the block" do
  # could be moved into top-level TestPoller, or into shared setup, etc.
  poller = Object.new
  def poller.poll(*) ; yield ; end

  foo = Foo.new("lol", :poller => poller)
  foo.pick_first

  assert foo.some_state_was_updated
  end
end
Run Code Online (Sandbox Code Playgroud)

@zenspider说:

注意:我是反嘲笑的。我在这件事上几乎是反对。恕我直言,如果您不能在不进行模拟的情况下进行测试,则可能是设计问题。根据下面的文本进行相应的校准。

我建议使用Liskov替代负责人(LSP),因为我专注于测试process_msg在这种情况下是否正确。这个想法很简单,是子类,可以覆盖有问题的方法,并在测试中使用子类。LSP表示测试子类等同于测试超类。

就轮询对象而言,该方法涉及三个方面(轮询,过滤和处理),您不应该测试其中一个(因为它是第三方代码)。我会重构为这样的东西:

class Foo
  # ....

  def poll
    @poller.poll(idle_timeout: 60) do |message|
      yield message
    end
  end

  def pick_first
    poll do |message|
      process_msg(message) if some_condition(message)
    end
  end
end
Run Code Online (Sandbox Code Playgroud)

然后测试很简单:

class TestFoo1 < Foo
  def poll
    yield 42 # or whatever
  end

  # ...
end

# ...

assert_equal 42, TestFoo1.new.pick_first # some_condition truthy
assert_nil       TestFoo2.new.pick_first # some_condition falsey
Run Code Online (Sandbox Code Playgroud)

有更短/更“鲁比”的方式来做到这一点,但是它们等同于上面的方法,并且上面的方法更好地说明了这一点。