15

I'm implementing unit tests for a family of functions that all share a number of invariants. For example, calling the function with two matrices produce a matrix of known shape.

I would like to write unit tests to test the entire family of functions for this property, without having to write an individual test case for each function (particularly since more functions might be added later).

One way to do this would be to iterate over a list of these functions:

import unittest
import numpy

from somewhere import the_functions
from somewhere.else import TheClass

class Test_the_functions(unittest.TestCase):
  def setUp(self):
    self.matrix1 = numpy.ones((5,10))
    self.matrix2 = numpy.identity(5)

  def testOutputShape(unittest.TestCase):
     """Output of functions be of a certain shape"""
     for function in all_functions:
       output = function(self.matrix1, self.matrix2)
       fail_message = "%s produces output of the wrong shape" % str(function)
       self.assertEqual(self.matrix1.shape, output.shape, fail_message)

if __name__ == "__main__":
  unittest.main()

I got the idea for this from Dive Into Python. There, it's not a list of functions being tested but a list of known input-output pairs. The problem with this approach is that if any element of the list fails the test, the later elements don't get tested.

I looked at subclassing unittest.TestCase and somehow providing the specific function to test as an argument, but as far as I can tell that prevents us from using unittest.main() because there would be no way to pass the argument to the testcase.

I also looked at dynamically attaching "testSomething" functions to the testcase, by using setattr with a lamdba, but the testcase did not recognize them.

How can I rewrite this so it remains trivial to expand the list of tests, while still ensuring every test is run?

Rajkumar S
  • 2,471
  • 5
  • 23
  • 28
saffsd
  • 23,742
  • 18
  • 63
  • 67
  • related question: http://stackoverflow.com/questions/32899/how-to-generate-dynamic-unit-tests-in-python – jfs Dec 07 '08 at 20:46

9 Answers9

11

Here's my favorite approach to the "family of related tests". I like explicit subclasses of a TestCase that expresses the common features.

class MyTestF1( unittest.TestCase ):
    theFunction= staticmethod( f1 )
    def setUp(self):
        self.matrix1 = numpy.ones((5,10))
        self.matrix2 = numpy.identity(5)
    def testOutputShape( self ):
        """Output of functions be of a certain shape"""
        output = self.theFunction(self.matrix1, self.matrix2)
        fail_message = "%s produces output of the wrong shape" % (self.theFunction.__name__,)
        self.assertEqual(self.matrix1.shape, output.shape, fail_message)

class TestF2( MyTestF1 ):
    """Includes ALL of TestF1 tests, plus a new test."""
    theFunction= staticmethod( f2 )
    def testUniqueFeature( self ):
         # blah blah blah
         pass

class TestF3( MyTestF1 ):
    """Includes ALL of TestF1 tests with no additional code."""
    theFunction= staticmethod( f3 )

Add a function, add a subclass of MyTestF1. Each subclass of MyTestF1 includes all of the tests in MyTestF1 with no duplicated code of any kind.

Unique features are handled in an obvious way. New methods are added to the subclass.

It's completely compatible with unittest.main()

S.Lott
  • 384,516
  • 81
  • 508
  • 779
  • I like this object-oriented solution. "Explicit is better than implicit" – muhuk Dec 07 '08 at 15:48
  • I don't like this because it introduces a whole heap of duplicated code. Since each of the functions is meant to observe the same invariant being tested, I'd like a way to express exactly this without having to lump them into a single testcase. Perhaps I should? Thanks for the suggestion though. – saffsd Dec 07 '08 at 22:14
  • Refactor common code up into a superclass. That's what superclasses are for. Your "common test" is precisely why we have superclasses and subclasses. – S.Lott Dec 08 '08 at 01:59
  • The issue is that it doesn't refactor. I'm working with classification algorithms, and I'm modelling each as a function with two input matrices and one output matrix. Most of these functions are not even python, they're thin wrappers. Perhaps I should assert inside rather than test outside? – saffsd Dec 09 '08 at 00:28
  • @saffsd: "it doesn't refactor"? What is "it? I'm talking about refactoring the tests into a single common superclass so each function has a subclass that assures that common features of the function have common methods in a test. – S.Lott Dec 09 '08 at 01:41
6

You don't have to use meta classes here. A simple loop fits just fine. Take a look at the example below:

import unittest

class TestCase1(unittest.TestCase):
    def check_something(self, param1):
        self.assertTrue(param1)

def _add_test(name, param1):
    def test_method(self):
        self.check_something(param1)
    setattr(TestCase1, 'test_' + name, test_method)
    test_method.__name__ = 'test_' + name
    
for i in range(0, 3):
    _add_test(str(i), False)

Once the for is executed, the TestCase1 has three test methods that are supported by both nose and unittest.

Peter Mortensen
  • 30,738
  • 21
  • 105
  • 131
Piotr Czapla
  • 25,734
  • 24
  • 99
  • 122
  • yeah i find metaclasses for purposes of "instrumenting" single-use classes never flies well, this is a much better approach. – Matt Joiner Feb 08 '10 at 15:55
5

You could use a metaclass to dynamically insert the tests. This works fine for me:

import unittest

class UnderTest(object):

    def f1(self, i):
        return i + 1

    def f2(self, i):
        return i + 2

class TestMeta(type):

    def __new__(cls, name, bases, attrs):
        funcs = [t for t in dir(UnderTest) if t[0] == 'f']

        def doTest(t):
            def f(slf):
                ut=UnderTest()
                getattr(ut, t)(3)
            return f

        for f in funcs:
            attrs['test_gen_' + f] = doTest(f)
        return type.__new__(cls, name, bases, attrs)

class T(unittest.TestCase):

    __metaclass__ = TestMeta

    def testOne(self):
        self.assertTrue(True)

if __name__ == '__main__':
    unittest.main()
Dustin
  • 89,080
  • 21
  • 111
  • 133
  • 1
    Thanks, this works. Only one slight quirk, nose is unable to see the testcases added by the metaclass. Any suggestions? – saffsd Dec 07 '08 at 07:00
  • I'm not familiar with nose. This adds the methods to the class, so I'm not sure what nose could be doing to miss them. I'd be interesting to find out what its magic is, though. – Dustin Dec 07 '08 at 16:48
  • cutting it bit fine with the use of `f` in the __new__ there, it's a bit obscure – Matt Joiner Feb 08 '10 at 15:51
  • @saffsd: It's because nose overrides certain load points from `unittest` to impose its own restrictions as well as inserting plugin points. In particular, nose has a selector that matches a function object's `__name__` attribute against certain regexes, etc. See: http://stackoverflow.com/questions/5176396/nose-unittest-testcase-and-metaclass-auto-generated-test-methods-not-discover/5177625#5177625 – Santa Mar 03 '11 at 07:03
5

If you're already using nose (and some of your comments suggest you are), just use Test Generators, which are the most straightforward way to implement parametric tests I've come across:

For example:

from binary_search import search1 as search

def test_binary_search():
    data = (
        (-1, 3, []),
        (-1, 3, [1]),
        (0,  1, [1]),
        (0,  1, [1, 3, 5]),
        (1,  3, [1, 3, 5]),
        (2,  5, [1, 3, 5]),
        (-1, 0, [1, 3, 5]),
        (-1, 2, [1, 3, 5]),
        (-1, 4, [1, 3, 5]),
        (-1, 6, [1, 3, 5]),
        (0,  1, [1, 3, 5, 7]),
        (1,  3, [1, 3, 5, 7]),
        (2,  5, [1, 3, 5, 7]),
        (3,  7, [1, 3, 5, 7]),
        (-1, 0, [1, 3, 5, 7]),
        (-1, 2, [1, 3, 5, 7]),
        (-1, 4, [1, 3, 5, 7]),
        (-1, 6, [1, 3, 5, 7]),
        (-1, 8, [1, 3, 5, 7]),
    )

    for result, n, ns in data:
        yield check_binary_search, result, n, ns

def check_binary_search(expected, n, ns):
    actual = search(n, ns)
    assert expected == actual

Produces:

$ nosetests -d
...................
----------------------------------------------------------------------
Ran 19 tests in 0.009s

OK
Peter Mortensen
  • 30,738
  • 21
  • 105
  • 131
Aaron Maenpaa
  • 119,832
  • 11
  • 95
  • 108
3

You could use some "data-driven testing" packages:

Peter Mortensen
  • 30,738
  • 21
  • 105
  • 131
jleeothon
  • 2,907
  • 4
  • 19
  • 35
  • DDT (data-driven tests) - yet another over-generic name for a library (like Python's [unittest](https://docs.python.org/3/library/unittest.html) and [nose](https://pypi.org/project/nose/) (not even spelled properly as a proper noun)). It just causes confusion. – Peter Mortensen Feb 05 '21 at 17:16
1

The metaclass code in a previous answer has trouble with nose, because nose's wantMethod in its selector.py file is looking at a given test method's __name__, not the attribute dict key.

To use a metaclass defined test method with nose, the method name and dictionary key must be the same, and prefixed to be detected by nose (i.e., with 'test_').

# Test class that uses a metaclass
class TCType(type):
    def __new__(cls, name, bases, dct):
        def generate_test_method():
            def test_method(self):
                pass
            return test_method

        dct['test_method'] = generate_test_method()
        return type.__new__(cls, name, bases, dct)

class TestMetaclassed(object):
    __metaclass__ = TCType

    def test_one(self):
        pass
    def test_two(self):
        pass
Peter Mortensen
  • 30,738
  • 21
  • 105
  • 131
0

Metaclasses is one option. Another option is to use a TestSuite:

import unittest
import numpy
import funcs

# get references to functions
# only the functions and if their names start with "matrixOp"
functions_to_test = [v for k,v in funcs.__dict__ if v.func_name.startswith('matrixOp')]

# suplly an optional setup function
def setUp(self):
    self.matrix1 = numpy.ones((5,10))
    self.matrix2 = numpy.identity(5)

# create tests from functions directly and store those TestCases in a TestSuite
test_suite = unittest.TestSuite([unittest.FunctionTestCase(f, setUp=setUp) for f in functions_to_test])


if __name__ == "__main__":
unittest.main()

Haven't tested. But it should work fine.

muhuk
  • 15,777
  • 9
  • 59
  • 98
  • 2
    unittest.main() doesn't automatically pick this up, and neither does nose. Also, FunctionTestCase calls setUp without arguments, and the functions_to_test need to be wrapped in something that asserts the test. – saffsd Dec 07 '08 at 22:09
0

I've read the metaclass example, and I liked it, but it was missing two things:

  1. How to drive it with a data structure?
  2. How to make sure that the test function is written correctly?

I wrote this more complete example, which is data-driven, and in which the test function is itself unit-tested.

import unittest

TEST_DATA = (
    (0, 1),
    (1, 2),
    (2, 3),
    (3, 5), # This intentionally written to fail
)


class Foo(object):

  def f(self, n):
    return n + 1


class FooTestBase(object):
  """Base class, defines a function which performs assertions.

  It defines a value-driven check, which is written as a typical function, and
  can be tested.
  """

  def setUp(self):
    self.obj = Foo()

  def value_driven_test(self, number, expected):
    self.assertEquals(expected, self.obj.f(number))


class FooTestBaseTest(unittest.TestCase):
  """FooTestBase has a potentially complicated, data-driven function.

  It needs to be tested.
  """
  class FooTestExample(FooTestBase, unittest.TestCase):
    def runTest(self):
      return self.value_driven_test

  def test_value_driven_test_pass(self):
    test_base = self.FooTestExample()
    test_base.setUp()
    test_base.value_driven_test(1, 2)

  def test_value_driven_test_fail(self):
    test_base = self.FooTestExample()
    test_base.setUp()
    self.assertRaises(
        AssertionError,
        test_base.value_driven_test, 1, 3)


class DynamicTestMethodGenerator(type):
  """Class responsible for generating dynamic test functions.

  It only wraps parameters for specific calls of value_driven_test.  It could
  be called a form of currying.
  """

  def __new__(cls, name, bases, dct):
    def generate_test_method(number, expected):
      def test_method(self):
        self.value_driven_test(number, expected)
      return test_method
    for number, expected in TEST_DATA:
      method_name = "testNumbers_%s_and_%s" % (number, expected)
      dct[method_name] = generate_test_method(number, expected)
    return type.__new__(cls, name, bases, dct)


class FooUnitTest(FooTestBase, unittest.TestCase):
  """Combines generated and hand-written functions."""

  __metaclass__ = DynamicTestMethodGenerator


if __name__ == '__main__':
  unittest.main()

When running the above example, if there's a bug in the code (or wrong test data), the error message will contain function name, which should help in debugging.

.....F
======================================================================
FAIL: testNumbers_3_and_5 (__main__.FooUnitTest)
----------------------------------------------------------------------
Traceback (most recent call last):
  File "dyn_unittest.py", line 65, in test_method
    self.value_driven_test(number, expected)
  File "dyn_unittest.py", line 30, in value_driven_test
    self.assertEquals(expected, self.obj.f(number))
AssertionError: 5 != 4

----------------------------------------------------------------------
Ran 6 tests in 0.002s

FAILED (failures=1)
Peter Mortensen
  • 30,738
  • 21
  • 105
  • 131
automaciej
  • 429
  • 3
  • 14
-1

The problem with this approach is that if any element of the list fails the test, the later elements don't get tested.

If you look at it from the point of view that, if a test fails, that is critical and your entire package is invalid, then it doesn't matter that other elements won't get tested, because 'hey, you have an error to fix'.

Once that test passes, the other tests will then run.

Admittedly there is information to be gained from knowledge of which other tests are failing, and that can help with debugging, but apart from that, assume any test failure is an entire application failure.

Singletoned
  • 5,089
  • 3
  • 30
  • 32
  • 4
    I recognize that, but another counter-argument is that if you are running tests in a batch, say overnight, you want to know where all of the failures are, not just the first one. – saffsd Aug 26 '09 at 10:53