views:

985

answers:

3

I'm trying to unit test some code that looks like this:

def main():
    parser = optparse.OptionParser(description='This tool is cool', prog='cool-tool')
    parser.add_option('--foo', action='store', help='The foo option is self-explanatory')
    options, arguments = parser.parse_args()
    if not options.foo:
        parser.error('--foo option is required')
    print "Your foo is %s." % options.foo
    return 0

if __name__ == '__main__':
   sys.exit(main())

With code that looks like this:

@patch('optparse.OptionParser')
def test_main_with_missing_p4clientsdir_option(self, mock_optionparser):
    #
    # setup
    #
    optionparser_mock = Mock()
    mock_optionparser.return_value = optionparser_mock
    options_stub = Mock()
    options_stub.foo = None
    optionparser_mock.parse_args.return_value = (options_stub, sentinel.arguments)
    def parser_error_mock(message):
        self.assertEquals(message, '--foo option is required')
        sys.exit(2)
    optionparser_mock.error = parser_error_mock

    #
    # exercise & verify
    #
    self.assertEquals(sut.main(), 2)

I'm using Michael Foord's Mock, and nose to run the tests.

When I run the test, I get:

  File "/Users/dspitzer/Programming/Python/test-optparse-error/tests/sut_tests.py", line 27, in parser_error_mock
    sys.exit(2)
SystemExit: 2

----------------------------------------------------------------------
Ran 1 test in 0.012s

FAILED (errors=1)

The problem is that OptionParser.error does a sys.exit(2), and so main() naturally relies on that. But nose or unittest detects the (expected) sys.exit(2) and fails the test.

I can make the test pass by adding "return 2" under the parser.error() call in main() and removing the sys.exit() call from parser_error_mock(), but I find it distasteful to modify the code under test to allow a test to pass. Is there a better solution?

Update: df's answer works, although the correct call is "self.assertRaises(SystemExit, sut.main)".

Which means the test passes whatever the number is in the sys.exit() in parser_error_mock(). Is there any way to test for the exit code?

BTW, the test is more robust if I add:

self.assertEquals(optionparser_mock.method_calls, [('add_option', ('--foo',), {'action': 'store', 'help': 'The foo option is self-explanatory'}), ('parse_args', (), {})])

at the end.

Update 2: I can test for the exit code by replacing "self.assertRaises(SystemExit, sut.main)" with:

try:
    sut.main()
except SystemExit, e:
    self.assertEquals(type(e), type(SystemExit()))
    self.assertEquals(e.code, 2)
except Exception, e:
    self.fail('unexpected exception: %s' % e)
else:
    self.fail('SystemExit exception expected')
+5  A: 

Will this work instead of assertEquals?

self.assertRaises(SystemExit, sut.main, 2)

This should catch the SystemExit exception and prevent the script from terminating.

dF
Brilliant! Thank you. But see my update to the question.
Daryl Spitzer
I've "unaccepted" your answer, since it's not quite correct. But since you gave me the insight that led me to the answer (see the updates to the question), I'll give you a couple days to edit your answer and then I'll accept.
Daryl Spitzer
A: 

Probably this question contains some new information:

http://stackoverflow.com/questions/309396/java-how-to-test-methods-that-call-system-exit

furtelwart
I read it quickly and while it's the same problem (in Java), I didn't see anything applicable here (to Python). Do you?
Daryl Spitzer
A: 

As noted in my updates to my question, I had to modify dF's answer to:

self.assertRaises(SystemExit, sut.main)

...and I came up with a few longer snippet to test for the exit code.

[Note: I accepted my own answer, but I will delete this answer and accept dF's if he updates his.]

Daryl Spitzer