1

I'm running unit tests for my program.py file. I found an error: python3 -m unittest: error: unrecognized arguments:.

I believe the error comes from the argparse library I'm using where the target code is expecting some arguments.

Target file: program.py:

import argparse

parse = argparse.ArgumentParser(description="Command line program.")
parse.add_argument("--key", type=str,
                   help="Enter key")
parse.add_argument("--output", type=str,
                   help="Path to place results.")
args = parse.parse_args()


def program():
    # Use args here 

def writefile():
    # Uses args and write to file 

if __name__ == "__main__":
    program()

Test file: program_test.py:

import unittest

import program

class TestProgram(unittest.TestCase):
    def setUp(self):
        self.argv_list = ["--key", "somefile.txt",
                          "--output", "myresultfile.txt"]

    def test_program_stuff(self):
        # See "Things I've tried" 
        program.writefile(...)

Command:

me@mylinux:myprogram$ env/bin/python3 -m unittest -v program_test.py 

usage: python3 -m unittest [-h] [--key KEY] [--output OUTPUT]
python3 -m unittest: error: unrecognized arguments: -v program_test.py

Things I've tried:

  • Mock the argparse.Namespace with argparse.Namespace(key="key.txt", output="result.txt")
  • Manipulate sys.args in test_program_stuff by sys.args.append(self.argv_list)

I've looked at solutions to unit testing argparse but none have helped so I'm thinking it may not be the same issue:

Mohamed Allal
  • 17,920
  • 5
  • 94
  • 97
engineer-x
  • 2,173
  • 2
  • 12
  • 25
  • -m and -v are for the unittest parser.. But they remain in `sys.argv`, so your parser sees them and complains. – hpaulj Sep 12 '22 at 05:57

2 Answers2

2

Note: I do realize that this is a duplicate of Pytest unrecognized arguments when importing file with argparse, however, that question was unfortunately not answered. It is also a duplicate of How to call function with argparse arguments in unittests?, however, he doesn't want to provide arguments and instead wants to call another function defined in the module.


Essentially, the problem can be reduced to the following:

# main.py

import argparse

parse = argparse.ArgumentParser()
parse.add_argument("--foo", action="store_true")

args = parse.parse_args()

and

# tests.py

import main

If we run that with python -m unittest ./tests.py, we receive the following output:

usage: python -m unittest [-h] [--foo FOO]
python -m unittest: error: unrecognized arguments: ./tests.py

The problem is that if you import something, all the top level code will run during the import. Usually, this isn't a problem because in a library you only really define functions to be used by other programs, however, in your case the parse.parse_args() runs.

This can be resolved by guarding this logic similar to what you already did:

import argparse

def main():
    parse = argparse.ArgumentParser()
    parse.add_argument("--foo")

    args = parse.parse_args()

if __name__ == "__main__":
    main()

Here, __name__ will contain the name of the module which would be "main" if it is imported or "__main__" if it is run directly. Therefore, the main() function will not be called during the unit test. Read more about this here.


However, in your specific case, it seems that you want to test the functionality of the main() function (or rather the code that you have in your main module.)

There are generally two ways to achieve this:

  1. You can simply accept the arguments in main():

    import argparse
    import sys
    
    def main(argv):
        parse = argparse.ArgumentParser()
        parse.add_argument("--foo", action="store_true")
    
        args = parse.parse_args(argv[1:])
    
    if __name__ == "__main__":
        main(sys.argv)
    

    Then you are able to provide these arguments in the unit test as well:

    import unittest
    
    import main
    
    class Tests(unittest.TestCase):
        def test_whatever(self):
            main.main(["main.py", "--foo"])
    
  2. You can use a mock-framework to change the value of sys.argv. In this situation would seem a bit over-engineered. However, if you are interested, that is answered here.

asynts
  • 2,213
  • 2
  • 21
  • 35
  • Interesting, so argparse does some matching between the included sys.argv or included sys.argv[...]? Since the order doesn't matter when providing sys.argv and .add_argument(). And thank you! This works. – engineer-x Sep 13 '22 at 05:25
  • I am not entirely sure what you mean. If you are referring to the `argv[1:]` this removes the first entry from the list which is the name of the script, e.g. `"./main.py"`. The whole `parse` object just configures a parser. The parser runs when you exectute `parse_args()` and it looks at `sys.argv` by default or at the provided argument. – asynts Sep 13 '22 at 05:41
  • 1
    No, I was commenting on the behavior of `parse_args()`. I mean I can provide my own input list with argv[1:]. Which means objects in `parse` and objects read by `parse_args()` must be matched up. It makes sense because flags that are passed in through cmdline but not listed in `add_argument` are considered invalid. – engineer-x Sep 13 '22 at 07:10
0

The parse.parse_args is outside of a method so will get run when the file is imported by the unittest file. This is why it is complaining that it doesn't have the expected arguments.

A solution is to move the parse_args into a function. e.g:

import argparse
import sys


def parse_args(sys_args):
    parse = argparse.ArgumentParser(description="Command line program.")
    parse.add_argument("--key", type=str,
                       help="Enter key")
    parse.add_argument("--output", type=str,
                       help="Path to place results.")
    return parse.parse_args(sys_args)


def program(key, output):
    # Use args here
    use_key(key)
    writefile(output)


def use_key(key):
    print(f"I have the key: {key}")


def writefile(filename):
    # Uses args and write to file
    print(f"I will write to file: {filename}")


if __name__ == "__main__":
    parsed_args = parse_args(sys.argv[1:])
    program(parsed_args.key, parsed_args.output)

This then allows things to test individually. e.g:

import io
import unittest
from unittest import mock

import program


class TestProgram(unittest.TestCase):
    def setUp(self):
        self.argv_list = ["--key", "somefile.txt",
                          "--output", "myresultfile.txt"]

    def test_program_parse(self):
        # See "Things I've tried"
        args = program.parse_args(self.argv_list)
        self.assertEqual("somefile.txt", args.key)
        self.assertEqual("myresultfile.txt", args.output)

    def test_program(self):
        with mock.patch('sys.stdout', new=io.StringIO()) as fake_out:
            program.use_key('val1')
            self.assertEqual('I have the key: val1\n', fake_out.getvalue())

    def test_writefile(self):
        with mock.patch('sys.stdout', new=io.StringIO()) as fake_out:
            program.writefile('junk.txt')
            self.assertEqual('I will write to file: junk.txt\n', fake_out.getvalue())


if __name__ == '__main__':
    unittest.main(verbosity=2)
ukBaz
  • 6,985
  • 2
  • 8
  • 31