[Dev] Unit test runner
Phillip J. Eby
pje at telecommunity.com
Wed Jan 26 13:03:09 PST 2005
Hi folks. Lisa has asked me to put together a unit test runner that will
allow selective running of multiple unit tests, for example to run just
fast unit tests, or to run all tests including functional tests. I have a
couple of different ways to do that, but I'd like to get some input from
the team as to what they'd like to see in such a runner. But before I get
to the questions, I need to lay out some background on Chandler's current
test setup, and the options that are available for how to proceed.
Currently, the way most of Chandler's tests are structured, you can run
individual test modules with either hardhat or unittest.py, e.g.:
unittest.py repository.tests.TestText
will run that specific test module, or:
unittest.py repository.tests.TestText.TestText.testBZ2Compressed
to run an individual test within the module. You can also use hardhat to
collect all test modules named 'Test*.py'.
So, I gather that what Lisa's asking for is the ability to also run a
selected subset of the total collection of tests, such as just the "strict"
unit tests that test subsystems in isolation, with individual runtimes of
about 10ms or less. (Most of Chandler's current tests are technically
speaking functional or integration tests, as they do not test subsystems in
isolation from each other. These types of tests are important also, but
because they take longer to run, it's useful to also have more
"isolationist" tests for use during development.)
Python's unittest module has a standard way of representing a collection of
tests, called a "suite". The Python doctest module can also create suites
from embedded and external doctests. (Embedded = doctests in docstrings,
external = doctests in text files.) If you write a function that returns a
test suite, then that function can be run with unittest.py. For example,
here's an excerpt from a unit test module in
PyProtocols (protocols.tests.test_dispatch):
from unittest import TestCase, makeSuite, TestSuite
import doctest
# actual test classes omitted for brevity
TestClasses = (
TestGraph, TestTests, ExpressionTests, SimpleGenerics, GenericTests,
)
def test_combiners():
return doctest.DocFileSuite(
'combiners.txt', optionflags=doctest.ELLIPSIS, package='dispatch',
)
def test_suite():
return TestSuite(
[test_combiners(), ] +
[makeSuite(t,'test') for t in TestClasses]
)
The two functions here, 'test_suite' and 'test_combiners' are functions
that return unittest "test suites". One of them creates an external
doctest that runs the tests in 'combiners.txt' (a tutorial text file), and
the other creates a test suite that combines that test with test suites for
each of the other test classes in the module.
Thus, to run all 50 unit tests specified by this test module, I can now use:
unittest.py protocols.tests.test_dispatch.test_suite
Or to run just the doctest, I can use:
unittest.py protocols.tests.test_dispatch.test_combiners
And of course I can still also specify an individual test case class or method.
There are also a couple of other ways to gather tests. For example, this
code defines a function that returns a suite of all the test classes found
in every module of the 'repository.tests' package:
from unittest import defaultTestLoader, TestSuite
import repository.tests
testNames = """
TestAlias
TestBZ2
TestBinary
TestDeepCopyRef
TestDelete
TestIndexes
TestItems
TestKinds
TestLiteralAttributes
TestMerge
TestMixins
TestMove
TestPerfWithRSS
TestPersistentCollections
TestRedirectToOrdering
TestRefDictAlias
TestReferenceAttributes
TestRepository
TestRepositoryBasic
TestSkipList
TestText
TestTypes
""".split()
def test_suite():
return TestSuite(
[defaultTestLoader.loadTestsFromName(name, repository.tests)
for name in testNames]
)
Of course, the names in 'testNames' could name individual test classes,
methods, or suite-generating functions. They can also be absolute module
names instead of ones relative to a particular location.
So as you can see, there are a lot of potential ways we could organize
this, with different tradeoffs depending on what tests people want to run
and how often, not to mention how often they need to change the list of the
tests. There are even tradeoffs depending on how many tests you put in a
single module.
Currently, I notice that most Chandler tests have only one test class per
module, and many of those actually only have one test method per test
case. I'm guessing that this is a byproduct of how long tests take to run,
coupled with the fact that hardhat (unlike unittest.py) doesn't allow
selecting an individual test within a module to run.
By contrast, using unittest.py to select and run individual tests, you can
actually include more tests in the same module, with no overhead penalty
for doing so. This will be important as we add fast-running unit tests, as
each unit test tests only a very small piece of functionality, and there
are therefore usually a lot of them. For example, PyProtocols has only
seven test modules, but these contain hundreds of test methods; a typical
run of those tests involves maybe 320 individual test methods, each with an
average of maybe half a dozen assertions being tested.
So, if we start adding unit tests (in the strict isolationist sense) to
increase the coverage-to-runtime ratio of Chandler's tests, it's likely
that we'll tend towards putting more tests per class and more classes per
module than is currently done. So, if we set up a test-gathering strategy
that works well for the current situation, it won't necessarily continue to
work long-term.
On the other hand, I don't want to burden anybody with a test-gathering
style that would work well in the future, but which might seem tedious for
today's more limited needs. So, it seems like the best thing for me to do
is throw all this out here for discussion and find out what approach the
team would prefer to take.
(P.S. I forgot to mention... right now 'application.tests' isn't a
package, so it needs an '__init__.py' if you want to use 'unittest.py' to
run any of the tests in it. I think all the other test locations are
already packages.)
More information about the Dev
mailing list