Testing python command line apps -- Running from within the projects w/o installing

Ben Finney ben+python at benfinney.id.au
Thu Oct 31 19:42:23 EDT 2013


Göktuğ Kayaalp <self at gkayaalp.com> writes:

> I'm using python to write command line apps from time to time. I
> wonder *what is the conventional way to test-run these apps from
> within the project itself, while developing, without installing*.

As with making any application testable: Make small functions that do
one thing well, with narrow, clearly-defined interfaces.

With testing the module which will be run as the program, though, there
are special considerations; in particular, command-line parsing and
system exit.

So the advice for testing the program's main module specialises to: Keep
the body of “if __name__ == '__main__':” to an absolute minimum. Put all
of the set-up and process-end functionality into discrete functions with
discrete purposes, clear return values, and explicit parameters.

That, for me, usually looks like this (or something with fewer moving
parts if the application doesn't need them)::

    import argparse

    class FrobApplication:
        """ An application to frobnicate spiggleworts. """

        def __init__(self):
            """ Set up a new instance of FrobApplication. """
            self.argument_parser = self.make_argument_parser()

        def make_argument_parser(self):
            """ Make an argument parser for this application. """
            parser = argparse.ArgumentParser(
                    description="Frobnicate one or more spiggleworts.")
            parser.add_argument(…)
            …
            self.argument_parser = parser

        def parse_args(self, argv):
            """ Parse command-line arguments to this application.

                :param argv: The full sequence of command-line arguments.
                :return: None.

                """
            self.program_name = argv[0]
            args = self.argument_parser.parse_args(argv[1:])
            self.foo_flag = …
                
        def run(self):
            """ Run the application. """
            self.argument_parser.parse_args(
            …


    def main(argv=None):
        """ Mainline code for this module.

            :param argv: The full sequence of command-line arguments. If
                ``None``, the Python runtime ``sys.argv`` is used.
            :return: The exit code for the process.

            """
        if argv is None:
            from sys import argv as sys_argv
            argv = sys_argv

        exit_code = 0
        try:
            frob_app = FrobApplication()
            frob_app.parse_args(argv)
            frob_app.main()
        except SystemExit, exc:
            exit_code = exc.code

        return exit_code

    if __name__ == '__main__':
        exit_code = __main__(sys.argv)
        sys.exit(exit_code)

This is adapted from an article by Guido van Rossum
<URL:http://www.artima.com/weblogs/viewpost.jsp?thread=4829>.

The purpose of the distinct module-level ‘main’ is to turn into a normal
function interface what Python usually handles in less-easily-tested
ways: You want to normally use ‘sys.argv’, which ‘argparse’ will do by
default; but you want the same code to also accept your own value for
testing. You normally want to raise ‘SystemExit’ from any appropriate
point in your application and have that exit the program with the exit
code; but when testing, you just want a return value.

This allows you to test each of the parts with a narrow interface: you
can call ‘main’ feeding it your constructed command line, and getting a
return value instead of a System Exit exception. You can test the
application as a class: just the application initialisation, just the
command-line parsing, just the main run routine. And so on.

Yes, it's a whole lot of scaffolding; and no, I don't always use it all.
But making testable code entails dividing your code into small, easily
isolated, easily testable units. Sometimes that means you need to set up
a bunch of divisions where normally Python takes care of it all — such
as in the handling of command-line arguments or the exit of the process.

-- 
 \          “That's all very good in practice, but how does it work in |
  `\                                             *theory*?” —anonymous |
_o__)                                                                  |
Ben Finney




More information about the Python-list mailing list