Search code examples
python-2.7unit-testingpython-unittesttornado-motor

Testing Motor calls with IOLoop


I'm running unittests in the callbacks for motor database calls, and I'm successfully catching AssertionErrors and having them surface when running nosetests, but the AssertionErrors are being caught in the wrong test. The tracebacks are to different files.

My unittests look generally like this:

def test_create(self):
    @self.callback
    def create_callback(result, error):
        self.assertIs(error, None)
        self.assertIsNot(result, None)
    question_db.create(QUESTION, create_callback)
    self.wait()

And the unittest.TestCase class I'm using looks like this:

class MotorTest(unittest.TestCase):
    bucket = Queue.Queue()
    # Ensure IOLoop stops to prevent blocking tests
    def callback(self, func):
        def wrapper(*args, **kwargs):
            try:
                func(*args, **kwargs)
            except Exception as e:
                self.bucket.put(traceback.format_exc())
            IOLoop.current().stop()
        return wrapper

    def wait(self):
        IOLoop.current().start()
        try:
            raise AssertionError(self.bucket.get(block = False))
        except Queue.Empty:
            pass

The errors I'm seeing:

======================================================================
FAIL: test_sync_user (app.tests.db.test_user_db.UserDBTest)
----------------------------------------------------------------------
Traceback (most recent call last):
  File "/Users/----/Documents/app/app-Server/app/tests/db/test_user_db.py", line 39, in test_sync_user
    self.wait()
  File "/Users/----/Documents/app/app-Server/app/tests/testutils/mongo.py", line 25, in wait
    raise AssertionError(self.bucket.get(block = False))
AssertionError: Traceback (most recent call last):
  File "/Users/----/Documents/app/app-Server/app/tests/testutils/mongo.py", line 16, in wrapper
    func(*args, **kwargs)
  File "/Users/----/Documents/app/app-Server/app/tests/db/test_question_db.py", line 32, in update_callback
    self.assertEqual(result["question"], "updated question?")
TypeError: 'NoneType' object has no attribute '__getitem__'

Where the error is reported to be in UsersDbTest but is clearly in test_questions_db.py (which is QuestionsDbTest)

I'm having issues with nosetests and asynchronous tests in general, so if anyone has any advice on that, it'd be greatly appreciated as well.


Solution

  • I can't fully understand your code without an SSCCE, but I'd say you're taking an unwise approach to async testing in general.

    The particular problem you face is that you don't wait for your test to complete (asynchronously) before leaving the test function, so there's work still pending in the IOLoop when you resume the loop in your next test. Use Tornado's own "testing" module -- it provides convenient methods for starting and stopping the loop, and it recreates the loop between tests so you don't experience interference like what you're reporting. Finally, it has extremely convenient means of testing coroutines.

    For example:

    import unittest
    from tornado.testing import AsyncTestCase, gen_test
    
    import motor
    
    # AsyncTestCase creates a new loop for each test, avoiding interference
    # between tests.
    class Test(AsyncTestCase):
        def callback(self, result, error):
            # Translate from Motor callbacks' (result, error) convention to the
            # single arg expected by "stop".
            self.stop((result, error))
    
        def test_with_a_callback(self):
            client = motor.MotorClient()
            collection = client.test.collection
            collection.remove(callback=self.callback)
    
            # AsyncTestCase starts the loop, runs until "remove" calls "stop".
            self.wait()
    
            collection.insert({'_id': 123}, callback=self.callback)
    
            # Arguments passed to self.stop appear as return value of "self.wait".
            _id, error = self.wait()
            self.assertIsNone(error)
            self.assertEqual(123, _id)
    
            collection.count(callback=self.callback)
            cnt, error = self.wait()
            self.assertIsNone(error)
            self.assertEqual(1, cnt)
    
        @gen_test
        def test_with_a_coroutine(self):
            client = motor.MotorClient()
            collection = client.test.collection
            yield collection.remove()
            _id = yield collection.insert({'_id': 123})
            self.assertEqual(123, _id)
            cnt = yield collection.count()
            self.assertEqual(1, cnt)
    
    if __name__ == '__main__':
        unittest.main()
    

    (In this example I create a new MotorClient for each test, which is a good idea when testing applications that use Motor. Your actual application must not create a new MotorClient for each operation. For decent performance you must create one MotorClient when your application begins, and use that same one client throughout the process's lifetime.)

    Take a look at the testing module, and particularly the gen_test decorator:

    http://tornado.readthedocs.org/en/latest/testing.html

    These test conveniences take care of many details related to unittesting Tornado applications.

    I gave a talk and wrote an article about testing in Tornado, there's more info here:

    http://emptysqua.re/blog/eventually-correct-links/