Spec testing EventMachine-based (Reactor) Code

amqp, eventmachine, minitest, ruby, testing

Solution

Sorry if this response is too pedantic, but I think you'll have a much easier time writing the tests and the library if you distinguish between your unit tests and your acceptance tests.

BDD vs. TDD

Be careful not to confuse BDD with TDD. While both are quite useful, it can lead to problems when you try to test every edge case in an acceptance test. For example, BDD is about testing what you're trying to accomplish with your service, which has more to do with what you're doing with the message queue than connecting to the queue itself. What happens when you try to connect to a non-existent message queue fits more into the realm of a unit test in my opinion. It's also worth pointing out that your service shouldn't be responsible for testing the message queue itself, since that's the responsibility of AMQP.

BDD

While I'm not sure what your service is supposed to do exactly, I would imagine your BDD tests should look something like:

- start the service (can do this in a separate thread in the tests if you need to)

- write something to the queue

- wait for your service to respond

- check the results of the service

In other words, BDD (or acceptance tests, or integration tests, however you want to think about them) can treat your app as a black box that is supposed to provide certain functionality (or behavior). The tests keep you focused on your end goal, but are more meant for ensuring one or two golden use cases, rather than the robustness of the app. For that, you need to break down into unit tests.

TDD

When you're doing TDD, let the tests guide you somewhat in terms of code organization. It's difficult to test a method that creates a new thread and runs EM inside that thread, but it's not so hard to unit test either of these individually. So, consider putting the main thread code into a separate function that you can unit test separately. Then you can stub out that method when unit testing the `connect` method. Also, instead of testing what happens when you try to connect to a bad server (which tests AMQP), you can test what happens when AMQP throws an error (which is your code's responsibility to handle). Here, your unit test can stub out the response of `AMQP.connect` to throw an exception.

Problem

I'm trying out the whole `BDD` approach and would like to test the `AMQP`-based aspect of a vanilla `Ruby` application I am writing. After choosing `Minitest` as the test framework for its balance of features and expressiveness as opposed to other aptly-named vegetable frameworks, I set out to write this spec: ``` # File ./test/specs/services/my_service_spec.rb # Requirements for test running and configuration require "minitest/autorun" require "./test/specs/spec_helper" # External requires # Minitest Specs for EventMachine require "em/minitest/spec" # Internal requirements require "./services/distribution/my_service" # Spec start describe "MyService", "A Gateway to an AMQP Server" do # Connectivity it "cannot connect to an unreachable AMQP Server" do # This line breaks execution, commented out # include EM::MiniTest::Spec # ... # (abridged) Alter the configuration by specifying # an invalid host such as "l0c@alho$t" or such # ... # Try to connect and expect to fail with an Exception MyApp::MyService.connect.must_raise EventMachine::ConnectionError end end ``` I have commented out the inclusion of the em-minitest-spec gem's functionality which should coerce the spec to run inside the `EventMachine` reactor, if I include it I run into an even sketchier exception regarding (I suppose) inline classes and such: `NoMethodError: undefined method 'include' for #<#<Class:0x3a1d480>:0x3b29e00>`. The code I am testing against, namely the `connect` method within that Service is based on this article and looks like this: ``` # Main namespace module MyApp # Gateway to an AMQP Server class MyService # External requires require "eventmachine" require "amqp" # Main entry method, connects to the AMQP Server def self.connect # Add debugging, spawn a thread Thread.abort_on_exception = true begin @em_thread = Thread.new { begin EM.run do @connection = AMQP.connect(@settings["amqp-server"]) AMQP.channel = AMQP::Channel.new(@connection) end rescue raise end } # Fire up the thread @em_thread.join rescue Exception raise end end # method connect end end # class MyService ``` The whole "exception handling" is merely an attempt to bubble the exception out to a place where I can catch/handle it, that didn't help either, with or without the `begin` and `raise` bits I still get the same result when running the spec: `EventMachine::ConnectionError: unable to resolve server address`, which actually is what I would expect, yet `Minitest` doesn't play well with the whole reactor concept and fails the test on ground of this `Exception`. The question then remains: How does one test `EventMachine`-related code using `Minitest`'s spec mechanisms? Another question has also been hovering around regarding `Cucumber`, also unanswered. Or should I focus on my main functionality (e.g. messaging and seeing if the messages get sent/received) and forget about edge cases? Any insight would truly help! Of course, it can all come down to the code I wrote above, maybe it's not the way one goes about writing/testing these aspects. Could be! Notes on my environment: `ruby 1.9.3p194 (2012-04-20) [i386-mingw32]` (yes, Win32 :>), `minitest 3.2.0`, `eventmachine (1.0.0.rc.4 x86-mingw32)`, `amqp (0.9.7)` Thanks in advance!

Original source