16

Currently I have many similar unittest TestCases. Each TestCase contains both data (input values + expected output values) and logic (call the SUT and compare the actual output with the expected output).

I would like to separate the data from the logic. Thus I want a base class that only contains the logic and a derived class that contains only the data. I came up with this so far:

import unittest

class MyClass():
    def __init__(self, input):
        self.input = input
    def get_result(self):
        return self.input * 2

class TestBase(unittest.TestCase):
    def check(self, input, expected_output):
        obj = self.class_under_test(input)
        actual_output = obj.get_result()
        self.assertEqual(actual_output, expected_output)

    def test_get_result(self):
        for value in self.values:
            self.check(value[0], value[1])

class TestMyClass(TestBase):
    def __init__(self, methodName='runTest'):
        unittest.TestCase.__init__(self, methodName)        
        self.class_under_test = MyClass
        self.values = [(1, 2), (3, 6)]

unittest.main(exit = False)

But this fails with the following error:

AttributeError: 'TestBase' object has no attribute 'values'

Two questions:

  • Is my 'design' any good?
  • What's still needed to get it working?
1
  • 3
    Note that it's unusual (in my experience) to use TestCase.__init__ for anything. Generally, initialization code goes in TestCase.setUp. Commented Feb 4, 2015 at 23:47

6 Answers 6

20

A little late here but recently came into the need to have unit test inheritence

The most elegant solution that I could find is this:

First - you need a base test class

class MyBaseUnitTest(unittest.TestCase):
    __test__ = False
    def test_someting(self):
        ...

    def test_something_else(self):
        ...

then to inherit that class and run tests:

class TestA(MyBaseUnitTest):
    __test__ = True

    def test_feature(self):
        pass
    def test_feature2(self):
        pass

This is the best, and easiset way to have a single viewset inheritence.

The issue I found with multiple inheritance is that when you try invoke methods like setUp() it will not be called on the base test class, so you have to call it in each class you write that extends the base class.

I hope that this will help somebody with this somewhere in the future.

BTW: This was done in python3 - I do not know how it will react in python2

UPDATE:

This is probably better and more pythonic

class MyBaseUnitTest(object):
    def test_someting(self):
        ...

    def test_something_else(self):
        ...

class TestA(MyBaseUnitTest, unittest.TestCase):

    def test_feature(self):
        pass
    def test_feature2(self):
        pass

So long as the base test class does not extend "unittest.TestCase", the test runner will not resolve these tests and they will not run in the suite. They will only run where the base class extends them.

Sign up to request clarification or add additional context in comments.

3 Comments

I see a potential problem with the code suggested in the update. Since the mixin MyBaseUnitTest doesn't inherit from unittest.TestCase one can't call self.assertEqual etc. in it.
@steinar You can call self.assertEqual but you don't get the type hints for it. This is because python's dynamic nature gives access to self.assertEqual after the child class has inherited unittest.TestCase
Looks like this is not __test__ anymore, but rather __unittest_skip__ as can be seen in the source code here.
6

To make this work as expected, you minimally need to:

  • Make sure the init method of your subclasses test cases match that of TestCase, i.e. __init__(self, methodName="runTest")
  • Add a super call in the init method of your subclasses e.g. super(TestMyClass, self).__init__(methodName)
  • Add a self argument to test_get_result, i.e. def test_get_result(self):

As for whether it's good design, remember, your tests act in part as documentation for how your code is meant to work. If you've got all the work hidden away in TestCase instance state, what it does will not be as obvious. You might be better off, say, writing a mixin class that has custom assertions that take inputs and expected outputs.

2 Comments

Could you give an example of a better design with the mixin?
This answer helped me solve a similar inheritance problem encountered with TestLoader().loadTestsFromModule(). Adding the modified __init__() did the trick for me.
5

The design is (more or less) fine -- the one "hiccup" is that when unittest looks at all TestCase classes and runs the methods that start with "test" on them. You have a few options at this point.

One approach would be to specify the class under test and values as attributes on the class. Here, if possible, you'll want the values to be immutable...

class TestBase(unittest.TestCase):

    def check(self, input, expected_output):
        obj = self.class_under_test(input)
        actual_output = obj.get_result()
        self.assertEqual(actual_output, expected_output)

    def check_all(self):
        for value in self.values:
            self.check(value[0], value[1])

class TestMyClass1(TestBase):
    values = ((1, 2), (3, 4))
    class_under_test = MyClass1

    def test_it(self):
        self.check_all()

class TestMyClass2(TestBase):
    values = (('a', 'b'), ('d', 'e'))
    class_under_test = MyClass2

    def test_it(self):
        self.check_all()

Comments

3

You are hitting test_get_result() twice. I do not think any test*() methods should be in TestBase at all. Rather, use TestBase to provide custom assertions, error formatters, test-data generators, etc. and keep the actual tests in TestMyClass.

Comments

2

Python's unittest.main() executes all tests from all test classes in the current namespace.

One way to not run tests directly in your base class is to move it to a different module (e.g. move TestBase to testbase.py):

In the file which calls unittest.main(), be sure to not import that class into your namespace (don't use from testbase import * or similar):

import testbase

class TestMyClass1(testbase.TestBase):
    values = ((1, 2), (3, 4))
    class_under_test = MyClass1

Comments

-2

Chat GPT gave me the following answer which works like a charm:


class MyBaseUnitTest(unittest.TestCase):
    
    def setUp(self):
       self.some_common_resource


    def test_someting(self):
        ...

    def test_something_else(self):
        ...

Then we inherit the base class:

class TestA(MyBaseUnitTest):
    def setUp(self):
        super().setUp()

    def test_feature(self):
        pass
    def test_feature2(self):
        pass

1 Comment

This is the solution I arrived at myself, before checking here. However, according to my present understanding, using a large language model to generate answers (even to edit an answer that you wrote yourself) is not allowed on Stack Overflow. It's a bit beyond the scope to explain what "poisoning the well" means here, but I believe that's the concern, policy-wise — even if it is a foregone conclusion at this point.

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.