Let's say you've written a Rails app that runs background jobs using Resque. How do you test those jobs? You can run them inline and check that what you expected to happen, actually happened:
setup do Resque.inline = true end def test_account_updated do_something_that_queues_a_background_job_to_update_an_account(test_account) assert_equal :updated, test_account.status end
That works, but it's missing something. You backgrounded that job for a reason -- maybe it's slow, or you don't want it to happen right away. If you put that code in a background job, you probably care more about the job being queued, and less about what it does when it runs. (Besides, you can test that part separately).
resque_unit to solve this problem.
resque_unit is a fake version of Resque. It intercepts your jobs and gives you some more assertions:
def test_account_updated do_something_that_queues_a_background_job_to_update_an_account(test_account) assert_queued UpdateAccount, [test_account.id] end
On the inside,
resque_unit rebuilt part of Resque's API to change how jobs were queued.
This was great for an initial implementation. It was fast, you didn't need Redis on your continuous integration server, and it was easy to understand. But, as reimplementations of an API tend to do, it fell behind. It got way more complicated. More bugs popped into GitHub Issues, and more code had to be borrowed from Resque itself.
Besides all that, there was a really big gotcha for new users: if you loaded
resque_unit before you loaded Resque,
resque_unit would stop working.
Looking at other options
My favorite way to write an easily testable client is to build swappable storage and network layers. For example, imagine if Resque had a
RedisJobStore and an
InMemoryJobStore, that each implemented to the same API. You could write most of your unit tests against the
InMemoryJobStore, and avoid the dependency and complication of Redis. But, since Resque is designed to work specifically with Redis, this wasn't an option.
Instead, the answer was to go a level deeper. What if the Redis client itself had both a
RedisStore and an
InMemoryStore? It turns out this is a thing that exists, called
fakeredis re-implements the entire Redis API. But instead of talking to a running Redis server, it works entirely in-memory. This is really impressive, and seemed worth a try.
Bringing fakeredis to resque_unit
If you were working with real Resque in development or production, how would you check that a job was queued?
You wouldn't have to write much extra code. You'd queue a job normally. You could look for a queued job with
peek. You'd check queue size with
size. And you'd clean up after yourself with
flushdb. If you wanted to run the jobs inside a queue you'd have to pretend you were a worker. But for the most part, you'd barely have to write code.
resque_unit, it was almost that easy. I got to remove a ton of code. And the rest of the code is a lot smaller, a lot simpler, and a lot less likely to break.
One last quirk
There was one last problem:
fakeredis entirely takes over your connection to Redis. That makes it a pretty terrible dependency for a gem to have. What if you wanted to use real Redis for most of your tests? If you require
resque_unit, all of a sudden you've changed a lot about how your tests run! And it gives me a headache to think about how hard that would be to debug.
So, when you require
resque_unit, it does a little dance to be as unobtrusive as it can:
# This is a little weird. Fakeredis registers itself as the default # redis driver after you load it. This might not be what you want, # though -- resque_unit needs fakeredis, but you may have a reason to # use a different redis driver for the rest of your test code. So # we'll store the old default here, and restore it afer we're done # loading fakeredis. Then, we'll point resque at fakeredis # specifically. default_redis_driver = Redis::Connection.drivers.pop require 'fakeredis' Redis::Connection.drivers << default_redis_driver if default_redis_driver
module Resque module TestExtensions # A redis connection that always uses fakeredis. def fake_redis @fake_redis ||= Redis.new(driver: :memory) end # Always return the fake redis. def redis fake_redis end
What can you take away from this?
When you're building a library that depends on a data store or service, think about making it swappable. It'll make your own tests easier to write, and it'll be clearer to your readers which features of the service you use.
Don't reimplement APIs on the surface level. Especially if you don't own it, and you don't control revisions to it. You'll do nothing but chase changes to it, and your implementation will usually be behind and a little broken.
And a good in-memory fake, in the right place, can make testing and development so much easier.