unittest: assertRaises() with an instance instead of a type

Steven D'Aprano steve+comp.lang.python at pearwood.info
Thu Mar 29 22:45:05 EDT 2012


On Thu, 29 Mar 2012 09:08:30 +0200, Ulrich Eckhardt wrote:
> Am 28.03.2012 20:07, schrieb Steven D'Aprano:

>> Secondly, that is not the right way to do this unit test. You are
>> testing two distinct things, so you should write it as two separate
>> tests:
> [..code..]
>> If foo does *not* raise an exception, the unittest framework will
>> handle the failure for you. If it raises a different exception, the
>> framework will also handle that too.
>>
>> Then write a second test to check the exception code:
> [...]
>> Again, let the framework handle any unexpected cases.
> 
> Sorry, you got it wrong, it should be three tests: 1. Make sure foo()
> raises an exception. 2. Make sure foo() raises the right exception. 3.
> Make sure the errorcode in the exception is right.
> 
> Or maybe you should in between verify that the exception raised actually
> contains an errorcode? And that the errorcode can be equality-compared
> to the expected value? :>

Of course you are free to slice it even finer if you like:

testFooWillRaiseSomethingButIDontKnowWhat
testFooWillRaiseMyException
testFooWillRaiseMyExceptionWithErrorcode
testFooWillRaiseMyExceptionWithErrorcodeWhichSupportsEquality
testFooWillRaiseMyExceptionWithErrorcodeEqualToFooError

Five tests :)

To the degree that the decision of how finely to slice tests is a matter 
of personal judgement and/or taste, I was wrong to say "that is not the 
right way". I should have said "that is not how I would do that test".

I believe that a single test is too coarse, and three or more tests is 
too fine, but two tests is just right. Let me explain how I come to that 
judgement.

If you take a test-driven development approach, the right way to test 
this is to write testFooWillFail once you decide that foo() should raise 
MyException but before foo() actually does so. You would write the test, 
the test would fail, and you would fix foo() to ensure it raises the 
exception. Then you leave the now passing test in place to detect 
regressions.

Then you do the same for the errorcode. Hence two tests.

Since running tests is (usually) cheap, you never bother going back to 
remove tests which are made redundant by later tests. You only remove 
them if they are made redundant by chances to the code. So even though 
the first test is made redundant by the second (if the first fails, so 
will the second), you don't remove it.

Why not? Because it guards against regressions. Suppose I decide that 
errorcode is no longer needed, so I remove the test for errorcode. If I 
had earlier also removed the independent test for MyException being 
raised, I've now lost my only check against regressions in foo().

So: never remove tests just because they are redundant. Only remove them 
when they are obsolete due to changes in the code being tested.

Even when I don't actually write the tests in advance of the code, I 
still write them as if I were. That usually makes it easy for me to 
decide how fine grained the tests should be: since there was never a 
moment when I thought MyException should have an errorcode attribute, but 
not know what that attribute would be, I don't need a *separate* test for 
the existence of errorcode.

(I would only add such a separate test if there was a bug that sometimes 
the errorcode does not exist. That would be a regression test.)

The question of the exception type is a little more subtle. There *is* a 
moment when I knew that foo() should raise an exception, but before I 
decided what that exception would be. ValueError? TypeError? Something 
else? I can write the test before making that decision:

def testFooRaises(self):
    try:
        foo()
    except:  # catch anything
        pass
    else:
        self.fail("foo didn't raise")


However, the next step is broken: I have to modify foo() to raise an 
exception, and there is no "raise" equivalent to the bare "except", no 
way to raise an exception without specifying an exception type.

I can use a bare raise, but only in response to an existing exception. So 
to raise an exception at all, I need to decide what exception that will 
be. Even if I start with a placeholder "raise BaseException", and test 
for that, when I go back and change the code to "raise MyException" I 
should change the test, not create a new test.

Hence there is no point is testing for "any exception, I don't care what" 
since I can't write code corresponding to that test case. Hence, I end up 
with two tests, not three and certainly not five.




-- 
Steven



More information about the Python-list mailing list