27

I have some basic setup/teardown code that I want to reuse in a whole bunch of unit tests. So I got the bright idea of creating some derived classes to avoid repeating code in every test class.

In so doing, I received two strange errors. One, I cannot solve. Here is the unsolvable one:

AttributeError: 'TestDesktopRootController' object has no attribute '_testMethodName'

Here is my base class:

import unittest
import twill
import cherrypy
from cherrypy._cpwsgi import CPWSGIApp


class BaseControllerTest(unittest.TestCase):

    def __init__(self):
        self.controller = None

    def setUp(self):
        app = cherrypy.Application(self.controller)

        wsgi = CPWSGIApp(app)

        twill.add_wsgi_intercept('localhost', 8080, lambda : wsgi)

    def tearDown(self):
        twill.remove_wsgi_intercept('localhost', 8080)

And here is my derived class:

import twill
from base_controller_test import BaseControllerTest

class TestMyController(BaseControllerTest):

    def __init__(self, args):
        self.controller = MyController()
        BaseControllerTest.__init__(self)

    def test_root(self):
        script = "find 'Contacts'"
        twill.execute_string(script, initial_url='http://localhost:8080/')

The other strange error is:

TypeError: __init__() takes exactly 1 argument (2 given)

The "solution" to that was to add the word "args" to my __init__ function in the derived class. Is there any way to avoid that?

Remember, I have two errors in this one.

101010
  • 14,866
  • 30
  • 95
  • 172

2 Answers2

64

It's because you're overriding __init__() incorrectly. Almost certainly, you don't want to override __init__() at all; you should do everything in setUp(). I've been using unittest for >10 years and I don't think I've ever overridden __init__().

However, if you really do need to override __init__(), remember that you don't control where your constructor is called -- the framework calls it for you. So you have to provide a signature that it can call. From the source code (unittest/case.py), that signature is:

def __init__(self, methodName='runTest'):

The safe way to do this is to accept any arguments and just pass 'em up to the base class. Here is a working implementation:

class BaseTest(unittest.TestCase):
    def __init__(self, *args, **kwargs):
        unittest.TestCase.__init__(self, *args, **kwargs)

    def setUp(self):
        print "Base.setUp()"

    def tearDown(self):
        print "Base.tearDown()"


class TestSomething(BaseTest):
    def __init__(self, *args, **kwargs):
        BaseTest.__init__(self, *args, **kwargs)
        self.controller = object()

    def test_silly(self):
        self.assertTrue(1+1 == 2)
Greg Ward
  • 1,604
  • 1
  • 13
  • 13
  • 1
    I'm curious...why not use init? In my situation, I'm subclassing Django's TestCase with a general purpose class, and then subclassing *that* to create specific tests. The only difference between the tests are in the class attributes which are a perfect thing to put in `__init__()`. What am I missing here? – mlissner Aug 26 '15 at 01:48
  • @mlissner: first off, you've got your terminology wrong. If you're setting an attribute in `__init__()`, it's not a class attr -- it's an instance attr. When you set something in class scope, outside of any methods, *then* it is a class attr. – Greg Ward Aug 28 '15 at 12:07
  • 1
    @mlissner: and second, it's very rare to override `TestCase.__init__()` because it is simply not necessary. For every test, `unittest` creates a new instance of test class (which invokes `__init__()`), calls `setUp()`, calls the `test*()` method being run, then calls `tearDown()`. `setUp()` is the official designated extension point, and every time `__init__()` is called, so is `setUp()`. – Greg Ward Aug 28 '15 at 12:10
  • 2
    Thanks @GregWard. So, in a word: Convention. Unittest is a weird tool and by convention it wants this kind of thing in `setUp`. So now it's a question of unittest's conventions vs. Pythons, I suppose. Thanks also for the terminology correction. – mlissner Aug 28 '15 at 13:49
1

In BaseController's __init__ you need to call unittest.TestCase's __init__ just like you did in TestMyController.

The call to construct a TestCase from the framework may be passing an argument. The best way to handle this for deriving classes is:

class my_subclass(parentclass):
    def __init__(self, *args, **kw):
        parentclass.__init__(self, *args, **kw)
        ...
KQ.
  • 922
  • 4
  • 8