Skip to content.

plope

Personal tools
You are here: Home » Members » chrism's Home » Unit Test Dependency Injections
 
 

Unit Test Dependency Injections

I'm sure I'm reinventing the wheel on some axis, but as the result of some thought, I've created a small unit testing dependency injection framework.

A couple of days ago I posted a long blog entry about how we (or at least I) tend to use the Zope Component Archictecture. I personally often use the ZCA to introduce dependency injection specifically for unit tests.

The types of dependency injection the ZCA allows for can be used far more generally than just for making unit testing easier. The ZCA works pretty well generally for unit testing dependency injection patterns. But in some cases ZCA assumptions may make the resulting tested code less understandable. In particular, if you put this code under test:

  from zope.component import queryUtility
  from zope.interface import implements
  from mypackage.interfaces import ICatalogQuery

  class CatalogQuery(object):
      implements(ICatalogQuery)
      def __call__(self, context, **kw):
          """ Does something complicated """

  query = CatalogQuery()

  def unit_under_test(context):
      query = queryUtility(ICatalogQuery, default=query)
      return query(context, a=1)

And then in the code doing the testing, you do this:

  import unittest
  from zope.component import getSiteManager

  class Test(unittest.TestCase):

      def setUp(self):
          sm = getSiteManager()
          sm.__init__()

      def test_it(self):
          from mypackage import unit_under_test
          from zope.component import getUtility
          from mypackage.interfaces import ICatalogQuery

          class DummyQuery(object):
              def __call__(self, *arg, **kw):
                  return [1]

          class DummyContext(object):
              pass

          query = DummyQuery()
          context = DummyContext()

          sm = getSiteManager()
          sm.registerUtility(query, ICatalogQuery)
          self.assertEqual(unit_under_test(context), [1])

You note above that the code under test uses a utility lookup to obtain the catalog query API. This is done purely for testing purposes; not for pluggability purposes. However, a casual reader of the above code may assume that the "ICatalogQuery" API is a ZCA "plug point", as advertised by its retrieval as a ZCA "utility". While it may be useful to have an API definition for ICatalogQuery, in most systems such an API is explicitly not pluggable. There's no expectation in most systems with a low-level API like this that an arbitrary user should be expected to be able to plug in a different implementation of ICatalogQuery at all. The getUtility code in the unit under test should not need to exist; it's a misdirection indicating that this is some sort of plug point that is meant to take alternate implementations.

For testing situations such as this one, I've created a package named repoze.depinj (see also http://svn.repoze.org/repoze.depinj/trunk/).

This package is meant to be used instead of the ZCA for unit testing purposes when there is exactly one implementation of the dependency being stubbed out, and the code just needs to be able to find an alternate testing implementation when the system is under test.

Using repoze.depinj, the above code under test becomes:

  from repoze.depinj import lookup

  class CatalogQuery(object):
      def __call__(self, context, **kw):
          """ Does something complicated """

  query = CatalogQuery()

  def unit_under_test(context):
      query = lookup(query)
      return query(context, a=1)

And the code doing the testing becomes:

  import unittest
  from repoze import depinj

  class Test(unittest.TestCase):

      def setUp(self):
          depinj.clear()

      def test_it(self):
          from mypackage import unit_under_test
          from mypackage import query

          class DummyQuery(object):
              def __call__(self, *arg, **kw):
                  return [1]

          class DummyContext(object):
              pass

          dummy_query = DummyQuery()
          context = DummyContext()
          depinj.inject(dummy_query, query)
          self.assertEqual(unit_under_test(context), [1])

While the code is really no more readable in isolation, we have avoided needing to define an ICatalogQuery Zope interface for this interaction. We don't actually use a Zope interface at all.

Note that while we don't need Zope interfaces to document this behavior, we can still document the CatalogQuery API with a Zope interface if we want to. But because we wouldn't use that interface as an adapter marker, it would be impossible for someone to misunderstand as a ZCA plugpoint.

By the way, the answer to "why not monkeypatch instead?" is because I don't own the module-scope codepath . The answer to "why not supply an optional unit_under_test keyword argument supplying the query implementation?" is because it makes generating documentation from source harder. It's also kinda fugly and doesn't account for callables that want to accept truly arbitrary **kw args.

Created by chrism
Last modified 2009-12-04 12:14 PM

Mocker

Hi Chris,

Looks nice! It reminds me a bit of the 'replace' function in mocker (http://labix.org/mocker), which does something similar (but always replaces the symbol with a Mock instance instead of whatever you want).

Martin