Patching out Registries in Odoo Tests

Image credit: Drunken Monkey on Flickr

The Odoo test runner is by default more of an integration test suite than a unit test suite, being a test runner that requires a database and a module to be installed for it to run the tests for said module.

There a number of reasons that this is less than ideal for those using a TDD approach, namely that for TDD to work you want:

  • Tests to be fast so you can test often and get feedback faster
  • Tests to work on a unit level so you're testing the code you're writing (Integration tests compliment this later)

The basics of an Odoo module test

In Odoo your modules' models & methods are available to other users via a registry. This gives more flexibility to developers who want to do things like extend or overwrite core functionality as well as develop a namespace that might not be reflected via the module structure.

However with the flexibility comes the compromise of speed. Using registries forces developers to have the database running so they can access the code, this works when running the server but sucks for testing as it adds an overhead. (There is a cool little buildout recipe from Anybox that allows Nose to see the Odoo module namespace if you use buildout).

A simple test looks like this:

from openerp.tests.common import TransactionCase

class TestTheFoo(TransactionCase):

    def setUp(self):
        super(TestTheFoo, self).setUp()
        self.module_reg = self.registry('my.odoo.module')

    def test_01_call_a_module_method(self):
        test_foo = self.module_reg.get_foo()  # should return 'foo'
        self.assertEqual(test_foo, 'foo', 'get_foo did not return foo')

In the above test I'm making sure that my module's method get_foo is returning the string foo. As this test only requires me to call a method that doesn't depend on any database interaction this is fine but if get_foo was doing a read of the res.user model for a user's name (from a demo fixture file included in the module) then my setUp method would look something like this:

def setUp(self):
    cr, uid = self.cr, self.uid
    super(TestTheFoo, self).setUp()
    self.module_reg = self.registry('my.odoo.module')
    self.user_reg = self.registry('res.user')
    test_uid = self.user_reg.search(cr, uid, [['name', '=', 'Test User']])
    if not test_uid
        raise ValueError('Test User not found in database')

 def test_01_call_a_module_method(self):
        test_foo = self.module_reg.get_foo()  # should return 'Test User'
        self.assertEqual(test_foo, 'Test User', 'get_foo did not return Test User')

If a demo fixture file isn't used then the user would be created in the database in the setUp method instead. This still requires database interaction though.

Integration testing

Integration tests are still an important set of tests to run as they allow developers to ensure that their code runs correctly when all the other dependencies of the system are in place and these tests can help catch when a lower level change in the code base affects your code.

So how do we speed things up

As mentioned early the need for a database, demo data fixtures and the module installed pushes you down a path of integration testing but there is a means to patch out registries. Every registry object has two methods that allow you to patch out the return values of a call to the original classes' methods. These methods are _patch_method and _revert_method which allow you to patch a method and revert to the original method respectively.

So going back to our example test as the get_foo method is calling the underlying read method on the res.user model we can just patch the read method and return the string we would hope for. This simulates the value having been in the database but doesn't add the overhead of actually having to go and get the value.

Here's an example of how to mock out a method on res.user in a test:

def res_user_read_mock(*args, **kwargs):
    return [{'name': 'Test User'}]

def setUp(self):
    super(TestTheFoo, self).setUp()
    self.module_reg = self.registry('my.odoo.module')
    self.user_reg = self.registry('res.user')
    self.user_reg._patch_method('read', res_user_read_mock)

def tearDown(self):
    self.user_reg._revert_method('read')

Here's an example of how to call the original method from inside the mock:

def res_user_read_mock(*args, **kwargs):
    return res_user_read_mock.origin(*args, **kwargs)

Here's an example of how to use the args and kwargs sent to the registry to send back conditional information.

def res_user_read_mock(*args, **kwargs):
    # Argument mapping = [class, cursor, user id, record id, fields to get]
    # In this case we want record id or args[3]
    if args[3] == 1:
        return [{'name': 'Test User'}]
    elif args[3] == 2:
        return [{'name': 'Another User'}]
    else:
        return []

Some finer details

As the methods only really replacing the method called they don't offer some of the niceties of using the mock library. You are unable to track the calls made to the mock method and the args & kwargs are not processed in a way that makes it easy to understand what they mean in regard to the original method's needs.

Here's a hack using global vars to solve the call tracking problem:

calls_to_mock = []

def res_user_read_mock(*args, **kwargs):
    global calls_to_mock.append({'args': args, 'kwargs': kwargs})
    if len(calls_to_mock) > 2
        return 'Some foo'

Speed increases using _patch_method

The speed increases gained using _patch_method over demo fixtures or building the data in the setUp step are immense in the tests as part of my example project mocking was 177 times quicker than creating data in setUp and 15 times quicker than using a demo data file.

Testing times from example project

Using the tests in my example project which tested a simple function to get a user name from res.users and hash the name using md5. I collected three runs and have provided an average so you can see the speed increase.

Run One

Testing method Time to run test suite
SetUp 0.354s
Demo Data 0.025s
Patch Method 0.002s

Run Two

Testing method Time to run test suite
SetUp 0.359s
Demo Data 0.033s
Patch Method 0.001s

Run Three

Testing method Time to run test suite
SetUp 0.354s
Demo Data 0.034s
Patch Method 0.002s

Average

Testing method Time to run test suite
SetUp 0.355s
Demo Data 0.030s
Patch Method 0.002s

Read more

You can download an example project from my Github. The _patch_method and _revert_method code can be found on Odoo's Github