How to test Python classes that depend on argparse?

对着背影说爱祢 提交于 2019-12-19 08:59:15

问题


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:


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



来源:https://stackoverflow.com/questions/42331049/how-to-test-python-classes-that-depend-on-argparse

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!