summaryrefslogtreecommitdiff
path: root/numpy/testing/parametric.py
blob: 43577d7d4127212cdeba7e7a6e2093c6921bb3f4 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
"""Support for parametric tests in unittest.

:Author: Fernando Perez

Purpose
=======

Briefly, the main class in this module allows you to easily and cleanly
(without the gross name-mangling hacks that are normally needed) to write
unittest TestCase classes that have parametrized tests.  That is, tests which
consist of multiple sub-tests that scan for example a parameter range, but
where you want each sub-test to:

* count as a separate test in the statistics.

* be run even if others in the group error out or fail.


The class offers a simple name-based convention to create such tests (see
simple example at the end), in one of two ways:

* Each sub-test in a group can be run fully independently, with the
  setUp/tearDown methods being called each time.

* The whole group can be run with setUp/tearDown being called only once for the
  group.  This lets you conveniently reuse state that may be very expensive to
  compute for multiple tests.  Be careful not to corrupt it!!!


Caveats
=======

This code relies on implementation details of the unittest module (some key
methods are heavily modified versions of those, after copying them in).  So it
may well break either if you make sophisticated use of the unittest APIs, or if
unittest itself changes in the future.  I have only tested this with Python
2.5.

"""
__docformat__ = "restructuredtext en"

import unittest

class ParametricTestCase(unittest.TestCase):
    """TestCase subclass with support for parametric tests.

    Subclasses of this class can implement test methods that return a list of
    tests and arguments to call those with, to do parametric testing (often
    also called 'data driven' testing."""

    #: Prefix for tests with independent state.  These methods will be run with
    #: a separate setUp/tearDown call for each test in the group.
    _indepParTestPrefix = 'testip'

    #: Prefix for tests with shared state.  These methods will be run with
    #: a single setUp/tearDown call for the whole group.  This is useful when
    #: writing a group of tests for which the setup is expensive and one wants
    #: to actually share that state.  Use with care (especially be careful not
    #: to mutate the state you are using, which will alter later tests).
    _shareParTestPrefix = 'testsp'

    def exec_test(self,test,args,result):
        """Execute a single test.  Returns a success boolean"""

        ok = False
        try:
            test(*args)
            ok = True
        except self.failureException:
            result.addFailure(self, self._exc_info())
        except KeyboardInterrupt:
            raise
        except:
            result.addError(self, self._exc_info())

        return ok

    def set_testMethodDoc(self,doc):
        self._testMethodDoc = doc
        self._TestCase__testMethodDoc = doc

    def get_testMethodDoc(self):
        return self._testMethodDoc

    testMethodDoc = property(fset=set_testMethodDoc, fget=get_testMethodDoc)

    def get_testMethodName(self):
        try:
            return getattr(self,"_testMethodName")
        except:
            return getattr(self,"_TestCase__testMethodName")

    testMethodName = property(fget=get_testMethodName)

    def run_test(self, testInfo,result):
        """Run one test with arguments"""

        test,args = testInfo[0],testInfo[1:]

        # Reset the doc attribute to be the docstring of this particular test,
        # so that in error messages it prints the actual test's docstring and
        # not that of the test factory.
        self.testMethodDoc = test.__doc__
        result.startTest(self)
        try:
            try:
                self.setUp()
            except KeyboardInterrupt:
                raise
            except:
                result.addError(self, self._exc_info())
                return

            ok = self.exec_test(test,args,result)

            try:
                self.tearDown()
            except KeyboardInterrupt:
                raise
            except:
                result.addError(self, self._exc_info())
                ok = False
            if ok: result.addSuccess(self)
        finally:
            result.stopTest(self)

    def run_tests(self, tests,result):
        """Run many tests with a common setUp/tearDown.

        The entire set of tests is run with a single setUp/tearDown call."""

        try:
            self.setUp()
        except KeyboardInterrupt:
            raise
        except:
            result.testsRun += 1
            result.addError(self, self._exc_info())
            return

        saved_doc = self.testMethodDoc

        try:
            # Run all the tests specified
            for testInfo in tests:
                test,args = testInfo[0],testInfo[1:]

                # Set the doc argument for this test.  Note that even if we do
                # this, the fail/error tracebacks still print the docstring for
                # the parent factory, because they only generate the message at
                # the end of the run, AFTER we've restored it.  There is no way
                # to tell the unittest system (without overriding a lot of
                # stuff) to extract this information right away, the logic is
                # hardcoded to pull it later, since unittest assumes it doesn't
                # change.
                self.testMethodDoc = test.__doc__
                result.startTest(self)
                ok = self.exec_test(test,args,result)
                if ok: result.addSuccess(self)

        finally:
            # Restore docstring info and run tearDown once only.
            self.testMethodDoc = saved_doc
            try:
                self.tearDown()
            except KeyboardInterrupt:
                raise
            except:
                result.addError(self, self._exc_info())

    def run(self, result=None):
        """Test runner."""

        #print
        #print '*** run for method:',self._testMethodName  # dbg
        #print '***            doc:',self._testMethodDoc  # dbg

        if result is None: result = self.defaultTestResult()

        # Independent tests: each gets its own setup/teardown
        if self.testMethodName.startswith(self._indepParTestPrefix):
            for t in getattr(self,self.testMethodName)():
                self.run_test(t,result)
        # Shared-state test: single setup/teardown for all
        elif self.testMethodName.startswith(self._shareParTestPrefix):
            tests = getattr(self,self.testMethodName,'runTest')()
            self.run_tests(tests,result)
        # Normal unittest Test methods
        else:
            unittest.TestCase.run(self,result)

#############################################################################
# Quick and dirty interactive example/test
if __name__ == '__main__':

    class ExampleTestCase(ParametricTestCase):

        #-------------------------------------------------------------------
        # An instrumented setUp method so we can see when it gets called and
        # how many times per instance
        counter = 0

        def setUp(self):
            self.counter += 1
            print 'setUp count: %2s for: %s' % (self.counter,
                                                self.testMethodDoc)

        #-------------------------------------------------------------------
        # A standard test method, just like in the unittest docs.
        def test_foo(self):
            """Normal test for feature foo."""
            pass

        #-------------------------------------------------------------------
        # Testing methods that need parameters.  These can NOT be named test*,
        # since they would be picked up by unittest and called without
        # arguments.  Instead, call them anything else (I use tst*) and then
        # load them via the factories below.
        def tstX(self,i):
            "Test feature X with parameters."
            print 'tstX, i=',i
            if i==1 or i==3:
                # Test fails
                self.fail('i is bad, bad: %s' % i)

        def tstY(self,i):
            "Test feature Y with parameters."
            print 'tstY, i=',i
            if i==1:
                # Force an error
                1/0

        def tstXX(self,i,j):
            "Test feature XX with parameters."
            print 'tstXX, i=',i,'j=',j
            if i==1:
                # Test fails
                self.fail('i is bad, bad: %s' % i)

        def tstYY(self,i):
            "Test feature YY with parameters."
            print 'tstYY, i=',i
            if i==2:
                # Force an error
                1/0

        def tstZZ(self):
            """Test feature ZZ without parameters, needs multiple runs.

            This could be a random test that you want to run multiple times."""
            pass

        #-------------------------------------------------------------------
        # Parametric test factories that create the test groups to call the
        # above tst* methods with their required arguments.
        def testip(self):
            """Independent parametric test factory.

            A separate setUp() call is made for each test returned by this
            method.

            You must return an iterable (list or generator is fine) containing
            tuples with the actual method to be called as the first argument,
            and the arguments for that call later."""
            return [(self.tstX,i) for i in range(5)]

        def testip2(self):
            """Another independent parametric test factory"""
            return [(self.tstY,i) for i in range(5)]

        def testip3(self):
            """Test factory combining different subtests.

            This one shows how to assemble calls to different tests."""
            return [(self.tstX,3),(self.tstX,9),(self.tstXX,4,10),
                    (self.tstZZ,),(self.tstZZ,)]

        def testsp(self):
            """Shared parametric test factory

            A single setUp() call is made for all the tests returned by this
            method.
            """
            return [(self.tstXX,i,i+1) for i in range(5)]

        def testsp2(self):
            """Another shared parametric test factory"""
            return [(self.tstYY,i) for i in range(5)]

        def testsp3(self):
            """Another shared parametric test factory.

            This one simply calls the same test multiple times, without any
            arguments.  Note that you must still return tuples, even if there
            are no arguments."""
            return [(self.tstZZ,) for i in range(10)]


    # This test class runs normally under unittest's default runner
    unittest.main()