How do you write tests for the argparse portion of a python module?
Hire the world's top talent on demand or became one of them at Toptal: https://topt.al/25cXVn
and get $2,000 discount on your first invoice
--------------------------------------------------
Take control of your privacy with Proton's trusted, Swiss-based, secure services.
Choose what you need and safeguard your digital life:
Mail: https://go.getproton.me/SH1CU
VPN: https://go.getproton.me/SH1DI
Password Manager: https://go.getproton.me/SH1DJ
Drive: https://go.getproton.me/SH1CT
Music by Eric Matyas
https://www.soundimage.org
Track title: Ominous Technology Looping
--
Chapters
00:00 How Do You Write Tests For The Argparse Portion Of A Python Module?
00:16 Accepted Answer Score 381
01:11 Answer 2 Score 33
01:40 Answer 3 Score 61
03:06 Answer 4 Score 24
03:27 Thank you
--
Full question
https://stackoverflow.com/questions/1816...
--
Content licensed under CC BY-SA
https://meta.stackexchange.com/help/lice...
--
Tags
#python #unittesting #argparse
#avk47
ACCEPTED ANSWER
Score 381
You should refactor your code and move the parsing to a function:
def parse_args(args):
parser = argparse.ArgumentParser(...)
parser.add_argument...
# ...Create your parser as you like...
return parser.parse_args(args)
Then in your main function you should just call it with:
parser = parse_args(sys.argv[1:])
(where the first element of sys.argv that represents the script name is removed to not send it as an additional switch during CLI operation.)
In your tests, you can then call the parser function with whatever list of arguments you want to test it with:
def test_parser(self):
parser = parse_args(['-l', '-m'])
self.assertTrue(parser.long)
# ...and so on.
This way you'll never have to execute the code of your application just to test the parser.
If you need to change and/or add options to your parser later in your application, then create a factory method:
def create_parser():
parser = argparse.ArgumentParser(...)
parser.add_argument...
# ...Create your parser as you like...
return parser
You can later manipulate it if you want, and a test could look like:
class ParserTest(unittest.TestCase):
def setUp(self):
self.parser = create_parser()
def test_something(self):
parsed = self.parser.parse_args(['--something', 'test'])
self.assertEqual(parsed.something, 'test')
ANSWER 2
Score 61
"argparse portion" is a bit vague so this answer focuses on one part: the parse_args method. This is the method that interacts with your command line and gets all the passed values. Basically, you can mock what parse_args returns so that it doesn't need to actually get values from the command line. The mock package can be installed via pip for python versions 2.6-3.2. It's part of the standard library as unittest.mock from version 3.3 onwards.
import argparse
try:
from unittest import mock # python 3.3+
except ImportError:
import mock # python 2.6-3.2
@mock.patch('argparse.ArgumentParser.parse_args',
return_value=argparse.Namespace(kwarg1=value, kwarg2=value))
def test_command(mock_args):
pass
You have to include all your command method's args in Namespace even if they're not passed. Give those args a value of None. (see the docs) This style is useful for quickly doing testing for cases where different values are passed for each method argument. If you opt to mock Namespace itself for total argparse non-reliance in your tests, make sure it behaves similarly to the actual Namespace class.
Below is an example using the first snippet from the argparse library.
# test_mock_argparse.py
import argparse
try:
from unittest import mock # python 3.3+
except ImportError:
import mock # python 2.6-3.2
def main():
parser = argparse.ArgumentParser(description='Process some integers.')
parser.add_argument('integers', metavar='N', type=int, nargs='+',
help='an integer for the accumulator')
parser.add_argument('--sum', dest='accumulate', action='store_const',
const=sum, default=max,
help='sum the integers (default: find the max)')
args = parser.parse_args()
print(args) # NOTE: this is how you would check what the kwargs are if you're unsure
return args.accumulate(args.integers)
@mock.patch('argparse.ArgumentParser.parse_args',
return_value=argparse.Namespace(accumulate=sum, integers=[1,2,3]))
def test_command(mock_args):
res = main()
assert res == 6, "1 + 2 + 3 = 6"
if __name__ == "__main__":
print(main())
ANSWER 3
Score 33
Make your main() function take argv as an argument rather than letting it read from sys.argv as it will by default:
# mymodule.py
import argparse
import sys
def main(args):
parser = argparse.ArgumentParser()
parser.add_argument('-a')
process(**vars(parser.parse_args(args)))
return 0
def process(a=None):
pass
if __name__ == "__main__":
sys.exit(main(sys.argv[1:]))
Then you can test normally.
import mock
from mymodule import main
@mock.patch('mymodule.process')
def test_main(process):
main([])
process.assert_call_once_with(a=None)
@mock.patch('foo.process')
def test_main_a(process):
main(['-a', '1'])
process.assert_call_once_with(a='1')
ANSWER 4
Score 24
I did not want to modify the original serving script so I just mocked out the sys.argv part in argparse.
from unittest.mock import patch
with patch('argparse._sys.argv', ['python', 'serve.py']):
... # your test code here
This breaks if argparse implementation changes but enough for a quick test script. Sensibility is much more important than specificity in test scripts anyways.