1

I'm having issue with arranging code to make it easily testable. I have 2 major modules in my code: cache generator and modifier builder, both have approximately the same level of complexity. Modifier builder is used in one of the methods of child objects of cache generator.

I already have full test suite which covers functionality of modifier builder. I want to add tests which cover all functionality of cache generator, but in order to significantly reduce complexity of these tests, i need to replace modifier builder with some stub, which returns predefined 'canned data' based on the arguments i passed to it.

My actual problem lies within the choosing the way of replacing real modifier builder with stub, which looks good code-wise and which is still convenient for testing. Take a look at following code:

Code from GitHub:

cacheGenerator / generator.py:

class CacheGenerator:
    def __init__(self, logger):
        ...
        self._converter = Converter(logger)

    def run(self, dataHandler):
        ...
        data = self._converter.convert(data)

cacheGenerator / converter.py:

class Converter:
    ...

    def convert(self, data):
        ...
        self._buildModifiers(data)

    def _buildModifiers(self, data):
        ...
        builder = ModifierBuilder(data['expressions'], self._logger)
        ...
           modifiers, buildStatus = builder.buildEffect(...)

What are the ways of replacing modifier builder with stub? I suppose at least next few variants exist:

  1. Changes to code: Instantiate modifier builder in converter's init() and assign its instance as object attribute. For testing - make subclass of real converter, overriding init(), where we substitute real modifier builder with stub, then subclassing cache generator, replacing real converter with subclassed one in similar way. However, such approach will require modification of modifier builder: i will need to split data loading from init() method, which is undesirable
  2. Like 1), but move parts of Converter()._buildModifiers() method, which work with modifier builder, to separate methods to make them easily overriddable
  3. Like 1), but specify just modifier builder class (not instance) in cleaner's init(). This allows us to keep modifier builder as-is.
  4. Passing modifier builder class from the very top of cache generator (so that class we need to replace for testing is controllable by cache generator instantiation)
  5. Any other variants exist? Like some import magic?

Some of variants among 1-4 look acceptable, but ideally I'd like to keep code as close as possible (to original), so I'm investigating alternative ways of stub'bing child objects.

abatishchev
  • 98,240
  • 88
  • 296
  • 433
DarkPhoenix
  • 103
  • 2
  • 7

2 Answers2

1

I usually prefer 2 since it makes the intention clear, is a small change and it might be useful for other code which reuses my work.

Alternatively, have a look at dependency injection or a factory that builds ModifierBuilders for you.

Lastly, you could use monkey patching by importing the module and then assigning a new value to the symbol:

import cacheGenerator.converter
cacheGenerator.converter.ModifierBuilder = ...

Of course, this changes the symbol for everyone (i.e. also for all other tests), so you need to save the old value and restore it after the test.

And if you feel bad/uneasy about this solution, then you're right: It's a desperate measure. Use it as a kind of last resort if you really can't change the original code.

Community
  • 1
  • 1
Aaron Digulla
  • 321,842
  • 108
  • 597
  • 820
  • Note: I'm not 100% sure about the nested import syntax :-) – Aaron Digulla Jan 23 '13 at 11:12
  • Well, i'd like to avoid monkeypatching at any cost too. By saying 'import magic' i meant something more testing-oriented, like whole virtual namespace where i can selectively import real modules from filesystem, and manually define modules i want to be used as stubs. By any chance, do you know if such possibility exists? – DarkPhoenix Jan 23 '13 at 20:41
  • The link provided for "dependency injection" points to a particular DI framework. I prefer doing DI manually. Prefer constructor injection, but you can also use setter injection. – Jon Reid Jan 24 '13 at 04:07
  • @DarkPhoenix: You could put those mock modules into certain folders outside of the default sources folders of your project (i.e. any place where Python would look). That would allow you to modify `sys.path` to make Python load your mocks. Unfortunately, this only works once; any `import` after that will get the same mock, you can't "flush" modules from the `import` cache. You could use a plugin framework like [pyplugin](http://code.google.com/p/pyplugin/) but that means changing the code as well. – Aaron Digulla Jan 24 '13 at 07:59
1

When I need to mock/fake objects in my tests I use Fudge.

In your case, I would recommend the use of patched_context. With it, you can patch calls to Converter methods.

You then can do this:

Patch calls to _converter.convert

test.py:

from cacheGenerator.generator import CacheGenerator
from cacheGenerator.converter import Converter
from fudge import patched_context

import unittest

class Test_cacheGenerator(unittest.testCase):

    def test_run(self):

        def fakeData(convertself, data):
            # Create data to be returned to 
            # data = self._converter.convert(data)
            fakedata = ...
            return fakedata


        # We tell Fudge to patch the call to `Converter.convert`
        # and instead call our defined function 
        cache = cacheGenerator(...)
        with patched_context(Converter, 'convert', fakeData)
            cache.run()

or you can patch calls to self._buildModifiers inside Converter:

def test_run(self):
        cache = cacheGenerator(...)

        def fakeBuildModifiers(convertself, data):
            # set up variables that convert._buildModifiers usually sets up
            convertself.modifiers = ...
            convertself.buildStatus = ...

        # We tell Fudge to patch the call to `Coverter._buildModifiers`
        # and instead call our defined function 
        cache = cacheGenerator(...)
        with patched_context(Converter, '_buildModifiers', fakeBuildModifiers):
            cache.run()

Alternatively, you can also use the Fudge fake object.

from fuge import Fake

...
    def test_run(self):
        cache = cacheGenerator(...)

        fakeData = ...
        fakeConverter = Fake('Converter').provides('convert').returns(fakeData)

        # Fake our `Converter` so that our any calls to `_converter.convert` are
        # made to `fakeConverter.convert` instead.
        cache._converter = fakeConverter

        cache.run()

In this last case, since you are patching the whole _converter object, if you're calling any other methods, you also need to patch them.

(Fake('Converter'.provides('convert').returns(fakeData)
                 .provides(....).returns()
                 .provides(....).returns()
)
Carlos
  • 2,222
  • 12
  • 21