Hopefully a simple question for MiniTest folks..
I have a section of code which I'll condense into an example here:
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
How can I mock/stub/something-else so that I can feed a message
through to be tested by the some_condition()
and possibly processed with process_msg()
?
I.e. I want to test the @poller.poll(idle_timeout: 60) do |message|
.
I have tried to stub the Aws::SQS::QueuePoller#new
with a mock poller, but it's not yielding the message to |message|
just returning it..
This is what I have, which is not working:
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
If I receive a variable in #pick_first
, that's where the mock puts it, not into |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
Answering my own question, in case someone else has the same question.
I asked for help on this via Twitter, and the author of MiniTest, Ryan Davis (aka @zenspider on github / @the_zenspider on Twitter) gave a quick answer along with an invite to submit the question to the MiniTest github issue tracker.
I did so, and got a couple of great responses, from Ryan and also from Pete Higgins (@phiggins on github), which I reproduce here in their entirety. Thank you to both of you for your help!
@phiggins said:
What about something like:
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
@zenspider said:
NOTE: I'm anti-mock. I'm almost anti-stub for that matter. IMHO, if you can't test something without mocking it, you probably have a design issue. Calibrate accordingly against the text below.
I suggested using Liskov Substitution Principal (LSP) because I was focused on testing that process_msg did the right thing in that context. The idea is simple, subclass, override the method in question, and use the subclass within the tests. LSP says that testing a subclass is equivalent to testing the superclass.
In the case of the polling object, you have three concerns (polling, filtering, and processing) going on in that method, one of whom you shouldn't be testing (because it is third-party code). I'd refactor to something like this:
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
Then testing is a simple matter:
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
There are shorter/"rubyier" ways to do this, but they're equivalent to the above and the above illustrates the point better.
I was trying to stub something that yields a block and had trouble finding the answer. (Unsure if this is exactly what you were asking.) Here's how to do it.
Here's our class that we want to mock:
class Foo
def bar
yield(42)
end
end
In our test we instantiate our object:
foo = Foo.new
Then we can override this method with plain Ruby to do something else:
def foo.bar
yield(16)
end
Now when we call it, it calls the stubbed version instead:
foo.bar do |value|
puts value
# => 16
end
Easy enough. This took me awhile to figure out. Hopefully this helps someone :)
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With