How to test or mock “if __name__ == '__main__'” contents

前端 未结 7 595
無奈伤痛
無奈伤痛 2020-12-07 20:07

Say I have a module with the following:

def main():
    pass

if __name__ == \"__main__\":
    main()

I want to write a unit test for the b

相关标签:
7条回答
  • 2020-12-07 20:44

    Whoa, I'm a little late to the party, but I recently ran into this issue and I think I came up with a better solution, so here it is...

    I was working on a module that contained a dozen or so scripts all ending with this exact copypasta:

    if __name__ == '__main__':
        if '--help' in sys.argv or '-h' in sys.argv:
            print(__doc__)
        else:
            sys.exit(main())
    

    Not horrible, sure, but not testable either. My solution was to write a new function in one of my modules:

    def run_script(name, doc, main):
        """Act like a script if we were invoked like a script."""
        if name == '__main__':
            if '--help' in sys.argv or '-h' in sys.argv:
                sys.stdout.write(doc)
            else:
                sys.exit(main())
    

    and then place this gem at the end of each script file:

    run_script(__name__, __doc__, main)
    

    Technically, this function will be run unconditionally whether your script was imported as a module or ran as a script. This is ok however because the function doesn't actually do anything unless the script is being ran as a script. So code coverage sees the function runs and says "yes, 100% code coverage!" Meanwhile, I wrote three tests to cover the function itself:

    @patch('mymodule.utils.sys')
    def test_run_script_as_import(self, sysMock):
        """The run_script() func is a NOP when name != __main__."""
        mainMock = Mock()
        sysMock.argv = []
        run_script('some_module', 'docdocdoc', mainMock)
        self.assertEqual(mainMock.mock_calls, [])
        self.assertEqual(sysMock.exit.mock_calls, [])
        self.assertEqual(sysMock.stdout.write.mock_calls, [])
    
    @patch('mymodule.utils.sys')
    def test_run_script_as_script(self, sysMock):
        """Invoke main() when run as a script."""
        mainMock = Mock()
        sysMock.argv = []
        run_script('__main__', 'docdocdoc', mainMock)
        mainMock.assert_called_once_with()
        sysMock.exit.assert_called_once_with(mainMock())
        self.assertEqual(sysMock.stdout.write.mock_calls, [])
    
    @patch('mymodule.utils.sys')
    def test_run_script_with_help(self, sysMock):
        """Print help when the user asks for help."""
        mainMock = Mock()
        for h in ('-h', '--help'):
            sysMock.argv = [h]
            run_script('__main__', h*5, mainMock)
            self.assertEqual(mainMock.mock_calls, [])
            self.assertEqual(sysMock.exit.mock_calls, [])
            sysMock.stdout.write.assert_called_with(h*5)
    

    Blam! Now you can write a testable main(), invoke it as a script, have 100% test coverage, and not need to ignore any code in your coverage report.

    0 讨论(0)
提交回复
热议问题