4

The below paste contains relevant snippets from three separate Python files. The first is a script called from the command line which instantiates CIPuller given certain arguments. What happens is that the script gets called with something like: script.py ci (other args to be swallowed by argparse).

The second is part of a subclass called Puller. The third is part of a subclass of Puller called CIPuller.

This works wonderfully, as the correct subclass is called, and any user using the wrong other args gets to see the correct args for their given subclass, plus the generic arguments from the superclass. (Although I was made aware offline that perhaps I should use argparse sub-commands for this.)

I'm stuck trying to write tests for these classes. Currently, I need an ArgumentParser to instantiate the classes, but in testing I'm not instantiating things from the command line, hence my ArgumentParser is useless.

I tried creating an ArgumentParser in the test harness to pass to CIPuller's constructor in the test code, but if I use add_argument there, argparse understandably complains about double (duplicate) arguments when it calls add_argument in the CIPuller constructor.

What would be a suitable design to test these classes with arguments?

#!/usr/bin/env python                                                             

from ci_puller import CIPuller                                                    
import argparse                                                                   
import sys                                                                        

# Using sys.argv[1] for the argument here, as we don't want to pass that onto     
# the subclasses, which should receive a vanilla ArgumentParser                   
puller_type = sys.argv.pop(1)                                                     

parser = argparse.ArgumentParser(                                                 
    description='Throw data into Elasticsearch.'                                  
)                                                                                 

if puller_type == 'ci':                                                           
    puller = CIPuller(parser, 'single')                                         
else:                                                                             
    raise ValueError("First parameter must be a supported puller. Exiting.")      

puller.run()                                                                      


class Puller(object):                                                             

    def __init__(self, parser, insert_type):                                      
        self.add_arguments(parser)                                                
        self.args = parser.parse_args()                                           

        self.insert_type = insert_type                                            

    def add_arguments(self,parser):
        parser.add_argument(                                                      
            "-d", "--debug",                                                      
            help="print debug info to stdout",                                    
            action="store_true"                                                   
        )                                                                         

        parser.add_argument(                                                      
            "--dontsend",                                                         
            help="don't actually send anything to Elasticsearch",                 
            action="store_true"                                                   
        )                                                                         

        parser.add_argument(                                                      
            "--host",                                                             
            help="override the default host that the data is sent to",            
            action='store',                                                       
            default='kibana.munged.tld'                                     
        )                             

class CIPuller(Puller):                                                           

    def __init__(self, parser, insert_type):                                      
        self.add_arguments(parser)

        self.index_prefix = "code"                                                
        self.doc_type = "cirun"                                                   

        self.build_url = ""                                                       
        self.json_url = ""                                                        
        self.result = []                                                          

        super(CIPuller, self).__init__(parser, insert_type)                       

    def add_arguments(self, parser):                                              
        parser.add_argument(                                                      
            '--buildnumber',                                                      
            help='CI build number',                                               
            action='store',                                                       
            required=True                                                         
        )                                                                         

        parser.add_argument(                                                      
            '--testtype',                                                         
            help='Job type per CI e.g. minitest / feature',                       
            choices=['minitest', 'feature'],                                      
            required=True                                                         
        )                                                                         

        parser.add_argument(                                                      
            '--app',                                                              
            help='App e.g. sapi / stats',                                         
            choices=['sapi', 'stats'],                                            
            required=True                                                         
        )                                                                         

1 Answer 1

9

Unittesting for argparse is tricky. There is a test/test_argparse.py file that is run as part of the overall Python unittest. But it has a complicated custom testing harness to handle most cases.

There are three basic issues, 1) calling parse_args with test values, 2) testing the resulting args, 3) testing for errors.

Testing the resulting args is relatively easy. And the argparse.Namespace class has simple __eq__ method so you can test one namespace against another.

There are two ways of testing inputs. One is to modify the sys.argv. Initially sys.argv has strings meant for the tester.

self.args = parser.parse_args()

tests sys.argv[1:] as a default. So if you change sys.argv you can test custom values.

But you can also give parse_args a custom list. The argparse docs uses this in most of its examples.

self.args = parser.parse_args(argv=myargv)

If myarg is None it uses sys.argv[1:]. Otherwise it uses that custom list.

Testing errors requires either a custom parse.error method (see docs) or wrapping the parse_args in a try/except block that can catch a sys.exit exception.

How do you write tests for the argparse portion of a python module?

python unittest for argparse

Argparse unit tests: Suppress the help message

Unittest with command-line arguments

Using unittest to test argparse - exit errors

Sign up to request clarification or add additional context in comments.

3 Comments

Modifying sys.argv for the test seems the easiest. Which is a more common practise, though, to modify sys.argv or to monkeypatch ArgumentParser to use your custom arvg by default?
Give Puller a separate parse method that takes an optional argv parameter that is passed to parse_args. Then you can use it in normal run mode, or with test values. test_argparse tests both approaches.
That was really helpful, even though I'd been over some of the links mentioned, the basic idea of setting sys.argv hadn't occurred to me. Sometimes we don't see the forest for the trees. :)

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.