RSS
The Testing Goat

Obey the Testing Goat!

TDD for the Web, with Python, Selenium, Django, JavaScript and pals...

How to unit test tornado ioloop callbacks

Mon 10 June 2013
By Harry

WARNING: this is not battle-tested wisdom of a massively experienced tornado tester. Today was the first time we ever tried to test something that actually uses the ioloop, and we've probably got it all totally backwards. Still, in case it helps...

Async. It's always hard to wrap your head around, so perhaps it's not surprising that it took us a few goes at work today before we got the hang of it.

Here's a bit of code that adds a callback to the tornado ioloop:

def sort_that_out(mess):
    IOLoop.instance().add_callback(mess.sort)

How might one naively write a test for it?

class TestSortingStuffOut(unittest.TestCase):

    def test_stuff_get_sorted(self):
        stuff = [3,2,1]
        sort_that_out(mess)
        self.assertEqual(mess, [1, 2, 3])

Well, that doesn't work:

AssertionError: Lists differ: [3, 2, 1] != [1, 2, 3]

A little head-scratching will get you to the fact that it's because the tornado IOLoop hasn't actually been started, so our callback never gets run. So, let's fix that:

def test_stuff_get_sorted(self):
    stuff = [3,2,1]
    sort_that_out(stuff)
    IOLoop.instance().start()
    self.assertEqual(stuff, [1, 2, 3])

What about now? The test hangs, and a little Ctrl-C based profiling tells us where the busy loop is:

  File "/tmp/t.py", line 13, in test_stuff_get_sorted     
    IOLoop.instance().start()
  File "/usr/local/lib/python2.7/site-packages/tornado/ioloop.py", line 627, in start
     event_pairs = self._impl.poll(poll_timeout) 
KeyboardInterrupt 

Right. start() on the IOLoop is a blocking call, and just assumes the loop should be run forever. At this point we ventured over to the official tornado testing docs but they seem to suggest a lot of overcomplicated things: using a self.wait, overriding get_new_ioloop to return the singleton...

Actually, all you really need to do is this:

def test_stuff_get_sorted(self):
    stuff = [3,2,1]

    sort_that_out(stuff)

    IOLoop.instance().add_callback(IOLoop.instance().stop)
    IOLoop.instance().start()

    self.assertEqual(stuff, [1, 2, 3])

We just add our own callback, telling the loop to shut itself down, making sure that it's the last callback added before we start the loop. Voila!

.
 ---------------------------------------------------------------------- 
Ran 1 test in 0.001s

OK 

Well, that was our first foray into writing a test for tornado that actually used the IOLoop (all our other tests have just mocked everything). No doubt the tornado tools come in useful for other use cases. And you'd probably want to use a tearDown or addCleanup that made sure the IOLoop got shut down even when your test doesn't behave as expected....

But I though I'd post this in case anybody else has a simple requirement to test a tornado async callback, and finds the docs a little hard-going. Hope it helps!

Comments

comments powered by Disqus
Read the book

I'm writing a book all about TDD and Web programming. Read the draft and let me know what you think!

Reviews & Testimonials

"Hands down the best teaching book I've ever read""Even the first 4 chapters were worth the money""Oh my gosh! This book is outstanding""The testing goat is my new friend"Read more...

Resources

A selection of links and videos about TDD, not necessarily all mine, eg this tutorial at PyCon 2013, how to motivate coworkers to write unit tests, thoughts on Django's test tools, London-style TDD and more.

Old TDD / Django Tutorial

This is my old TDD tutorial, which follows along with the official Django tutorial, but with full TDD. It badly needs updating. Read the book instead!

Save the Testing Goat Campaign

The campaign page, preserved for history, which led to the glorious presence of the Testing Goat on the front of the book.