9

Background: I am working on a web scraper to track prices at online stores. It uses Django. I have a module for each store, with functions like get_price() and get_product_name() written for each one, so that the modules can be used interchangeably by the main scraper module. I have store_a.py, store_b.py, store_c.py, et cetera, each with these functions defined.

In order to prevent duplication of code, I've made StoreTestCase, which inherits from TestCase. For each store, I have a subclass of StoreTestCase, like StoreATestCase and StoreBTestCase.

When I manually test the StoreATestCase class, the test runner does what I want. It uses the data in the child class self.data for its tests, and doesn't attempt to set up and test the parent class on its own:

python manage.py test myproject.tests.test_store_a.StoreATest

However, when I manually test against the module, like:

python manage.py test myproject.tests.test_store_a

It first runs the tests for the child class and succeeds, but then it runs them for the parent class and returns the following error:

    for page in self.data:
TypeError: 'NoneType' object is not iterable

store_test.py (parent class)

from django.test import TestCase

class StoreTestCase(TestCase):

    def setUp(self):
        '''This should never execute but it does when I test test_store_a'''
        self.data = None
    def test_get_price(self):
        for page in self.data:
            self.assertEqual(store_a.get_price(page['url']), page['expected_price'])

test_store_a.py (child class)

import store_a
from store_test import StoreTestCase

class StoreATestCase(StoreTestCase):

    def setUp(self):
        self.data = [{'url': 'http://www.foo.com/bar', 'expected_price': 7.99},
                     {'url': 'http://www.foo.com/baz', 'expected_price': 12.67}]

How do I ensure the Django test runner only tests the child class, and not the parent class?

Stewart
  • 1,659
  • 4
  • 23
  • 35
  • 1
    if you don't call `super` or the `StoreTestCase.__init__` directly, it should never execute it since it's been overridden. – Paco Apr 28 '15 at 11:26

3 Answers3

11

One way to fix this is to use Mixins:

from django.test import TestCase

class StoreTestCase(object):

    def setUp(self):
        '''This should never execute but it does when I test test_store_a'''
        self.data = None
    def test_get_price(self):
        for page in self.data:
            self.assertEqual(store_a.get_price(page['url']), page['expected_price'])

class StoreATestCase(StoreTestCase, TestCase):

    def setUp(self):
        self.data = [{'url': 'http://www.foo.com/bar', 'expected_price': 7.99},
                     {'url': 'http://www.foo.com/baz', 'expected_price': 12.67}]

The StoreTestCase will not be executed since it is not a TestCase, but your StoreATestCase will still benefit from the inheritance.

I think that your issue happens because StoreTestCase is a TestCase instance, so it gets executed when you run the tests.

Edit:

I would also suggest to raise an exception in StoreTestCase.setUp, explicitly saying that is not implemented. Have a look at these exception. You would end up with something like this:

import exceptions  # At the top of the file

[...]

def setUp(object):
    raise exceptions.NotImplementedError('Please override this method in your subclass')
Community
  • 1
  • 1
Paco
  • 4,520
  • 3
  • 29
  • 53
  • 2
    Isn't it invalid to do self.assertEqual() on StoreTestCase(object) since it doesn't have that method? – Stewart Apr 28 '15 at 21:20
  • It is perfectly valid, it will use the `assertEqual` from `StoreATestCase`. Have a look at the link in the answer (Mixins). – Paco Apr 29 '15 at 08:29
5

You can to hide base class inside another:

store_test.py (parent class)

from django.test import TestCase

class TestHelpers(object):
    class StoreTestCase(TestCase):
    ...

test_store_a.py (child class)

import store_a
from store_test import TestHelpers

class StoreATestCase(TestHelpers.StoreTestCase):
    ...
Symon
  • 1,626
  • 1
  • 23
  • 31
2

If you want to avoid the Multi-inheritance, this is a possible solution as well. Django Test Cases are not called via the init constructor so the setUp Method has to be overridden:

from unittest import SkipTest
from django.test import TestCase

class BaseTest(TestCase):
    def setUp(self):
        if self.__class__ == BaseTest:
            raise SkipTest('Abstract test')
        your_stuff = 'here'
...

The only downside is, that the skipped test will be mentioned in your test report. Unittest documentation: https://docs.python.org/dev/library/unittest.html#unittest.SkipTest