Unit-testing single function with large number of different inputs

E

Edvard Majakari

Hi all ya unit-testing experts there :)

Code I'm working on has to parse large and complex files and detect
equally complex and large amount of errors before the contents of the file
is fed to module interpreting it. First I created a unit-test class named
TestLoad which loaded, say, 40 files of which about 10 are correct and
other 30 files contained over 20 different types of errors. Different
methods on the TestLoad class were coded so that they expected the
spesific error to occur. They looked like the following:

def test_xxx_err(self):
for fname in xxx_err_lst:
self.assertEqual(get_load_status(fname), xxx)

However, now we've added several more tests and we have added another
function to the system - the ability to try loading the file without
actually executing instructions in it. So, I added another class
TestValidate, which has exactly the same tests as the TestLoad class, the
only difference being in the routine get_load_status, which is not
get_load_status but get_validate_status.

I thought I'd simplify the process of adding new tests - if it is too
cumbersome to add new tests, we'll end up with having few tests which in
turn will result in more buggy software system.

Thus I came up with the simple idea of adding two directories:
test/correct-files and test/incorrect-files. Every time the test suite is
run, it goes through every file in the test/correct-files and
test/incorrect-files and expects to receive status 0 from all files under
correct-files, and appropriate error code from all files under
test/incorrect-files. How does it deduce the correct error code? Well,
I've forced things to that any file in the test/incorrect-files must
begin with code prefix 'xx-' in the file name, and the test suite reads
expected error code from the file name prefix.

All this allows me to have only two methods (in addition to setUp and
tearDown methods) in TestLoad and TestValidate classes, and what's more
convenient - I don't have to alter unit test suite at all when I add new
file tests to the system; all I have to do is to put it under either
directory, and if the file contains an error, I prefix the file name with
appropriate code.

The system has but one annoyance: first, if I use self.assertEqual in the
test_load method, it aborts on the first error found and as such doesn't
test all the other files. This would be bad, because when errors occur in
the code, I have to fix them one by one running test suite after every fix
to find other potential errors in the system.

So I changed the test_load routine by replacing self.assertEqual with my
own code which won't abort the test if errors are found, but it will
report them with sufficient verbosity and continue testing. But this
approach isn't optimal either: because there is no self.assertEqual in the
test anymore, there may be many errors in the test class but after running
all the methods, the test suite will report OK status, even though errors
did occur.

The problem I described above shows probably one of the best reasons why
each test method should test only one (type of) thing, but in my special
case I don't find that appropriate due to aforementioned reasons. Any
suggestions? I was wondering about creating test methods dynamically
according to the files found in the test directories (which would solve
all the problems), but I find it a tad too complex way of solving the
problem.

--
# Edvard Majakari Software Engineer
# PGP PUBLIC KEY available Soli Deo Gloria!

$_ = '456476617264204d616a616b6172692c20612043687269737469616e20'; print
join('',map{chr hex}(split/(\w{2})/)),uc substr(crypt(60281449,'es'),2,4),"\n";
 
R

Robert Ferrell

I'm not a unit-testing expert, and I don't necessarily recommend what
I'm about to suggest, but...

To solve the problem of having a single "test" actually run many
"internal" tests, report on the results of those many tests, and fail
(in the PyUnit sense) if any of those internal tests fail, you could
add a flag, AllTestsPassed, to the TestLoad and TestValidate classes.
Initialize that flag to True. Then, in your version of assertEqual,
if any test fails set the flag to False. Finally, after you've run
all your tests, add

self.failUnless(self.AllTestsPassed, 'Some tests failed.')

HTH,

-robert
 
E

Edvard Majakari

add a flag, AllTestsPassed, to the TestLoad and TestValidate classes.
Initialize that flag to True. Then, in your version of assertEqual,
if any test fails set the flag to False. Finally, after you've run
all your tests, add

self.failUnless(self.AllTestsPassed, 'Some tests failed.')

But that doesn't give instant feedback (running ValidateTest takes over a
minute), neither does it report exact cause of errors. Hmm. I think I'll
look into Python's ability to create methods on the fly.

Thanks anyway!

PS. Sorry for the provoking .signature, it dates back to the Old Times
when I didn't know of Python ;)

--
#!/usr/bin/perl -w
$h={23,69,28,'6e',2,64,3,76,7,20,13,61,8,'4d',24,73,10,'6a',12,'6b',21,68,14,
72,16,'2c',17,20,9,61,11,61,25,74,4,61,1,45,29,20,5,72,18,61,15,69,20,43,26,
69,19,20,6,64,27,61,22,72};$_=join'',map{chr hex $h->{$_}}sort{$a<=>$b}
keys%$h;m/(\w).*\s(\w+)/x;$_.=uc substr(crypt(join('',60,28,14,49),join'',
map{lc}($1,substr $2,4,1)),2,4)."\n"; print;
 
P

Peter Otten

Edvard said:
The problem I described above shows probably one of the best reasons why
each test method should test only one (type of) thing, but in my special
case I don't find that appropriate due to aforementioned reasons. Any
suggestions? I was wondering about creating test methods dynamically
according to the files found in the test directories (which would solve
all the problems), but I find it a tad too complex way of solving the
problem.

I think dynamically creating test cases is the way to go. I would subclass
TestCase to test one file and then for every file put an instance of
MyTestCase into a TestSuite. For instance:

import unittest, os

# shared implementation
class Base(unittest.TestCase):
def __init__(self, filename):
unittest.TestCase.__init__(self)
self.filename = filename

# some pointless tests
class Good1(Base):
def runTest(self):
self.assertEqual(self.filename, self.filename.lower())

class Good2(Base):
def runTest(self):
self.assertEqual(self.filename.endswith(".py"), True)

class Bad(Base):
def runTest(self):
self.assertEqual(self.filename.endswith(".pyc"), True)

# build a test suite with (all applicable test cases)
# x (every file in a folder, optionally including subfolders)
def makeTests(classes, folder, recursive=False):
suite = unittest.TestSuite()
for path, folders, files in os.walk(folder):
for fn in files:
for cls in classes:
suite.addTest(cls(os.path.join(path, fn)))
if not recursive: break
return suite

# build the main test suite with subsuites for every folder
def makeSuite():
suite = unittest.TestSuite()
goodfolder = "/usr/local/lib/python2.3"
badfolder = "/usr/local/lib/python2.3"
suite.addTest(makeTests((Good1, Good2), goodfolder))
suite.addTest(makeTests((Bad,), badfolder))
return suite

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

Peter
 
E

Edvard Majakari

Peter Otten said:
I think dynamically creating test cases is the way to go. I would subclass
TestCase to test one file and then for every file put an instance of
MyTestCase into a TestSuite. For instance:

[snip]

Right!

What a nice solution - now I have good sides of my former and latter
solutions, and none of the bad sides (with the probable exception of
testing code being harder to read for others :)

Thank you.

--
# Edvard Majakari Software Engineer
# PGP PUBLIC KEY available Soli Deo Gloria!

$_ = '456476617264204d616a616b6172692c20612043687269737469616e20'; print
join('',map{chr hex}(split/(\w{2})/)),uc substr(crypt(60281449,'es'),2,4),"\n";
 
E

Edvard Majakari

Hm, one more question about this:
import unittest, os

# shared implementation
class Base(unittest.TestCase):
def __init__(self, filename):
unittest.TestCase.__init__(self)
self.filename = filename

# some pointless tests
class Good1(Base):
def runTest(self):
self.assertEqual(self.filename, self.filename.lower())

class Good2(Base):
def runTest(self):
self.assertEqual(self.filename.endswith(".py"), True)

class Bad(Base):
def runTest(self):
self.assertEqual(self.filename.endswith(".pyc"), True)

now, running tests with the -v flag I don't see the neat docstrings I've
used everywhere, but class instance strings (which is not that neat).
Then again, it doesn't really matter, but still..

Doing

# shared implementation
class Base(unittest.TestCase):
def __init__(self, filename):
unittest.TestCase.__init__(self)
self.filename = filename
self.runTest.__doc__ = "fiddling with file %s" self.filename

# some pointless tests
class Good1(Base):
def runTest(self):
self.assertEqual(self.filename, self.filename.lower())

doesn't work, because doc strings are read-only (which is good in general)

Now, it really doesn't matter a lot, I just thought it would be neater and
I'm also quite sure Python is able to do this, if I only knew how.

--
# Edvard Majakari Software Engineer
# PGP PUBLIC KEY available Soli Deo Gloria!

$_ = '456476617264204d616a616b6172692c20612043687269737469616e20'; print
join('',map{chr hex}(split/(\w{2})/)),uc substr(crypt(60281449,'es'),2,4),"\n";
 
P

Peter Otten

Edvard said:
now, running tests with the -v flag I don't see the neat docstrings I've
used everywhere, but class instance strings (which is not that neat).
Then again, it doesn't really matter, but still..

Just override the __str__() method to your needs, e. g:

class Base(unittest.TestCase):
def __init__(self, filename):
unittest.TestCase.__init__(self)
self.filename = filename
def __str__(self):
return self.filename

Peter
 

Ask a Question

Want to reply to this thread or ask your own question?

You'll need to choose a username for the site, which only take a couple of moments. After that, you can post your question and our members will help you out.

Ask a Question

Members online

No members online now.

Forum statistics

Threads
473,995
Messages
2,570,228
Members
46,818
Latest member
SapanaCarpetStudio

Latest Threads

Top