1

I'd like to write unit tests for the following function:

#!/usr/bin/env python3

"""IPv4 validation using `ipaddress module` and argparse."""

import argparse
from ipaddress import ip_address

def parse_cli_args():
    """
    Command line parser for subnet of interest.

    Args:
      --ip 0.0.0.0

    Returns:
      String, e.g. 0.0.0.0

    """

    parser = argparse.ArgumentParser(description="IPv4 address of interest.")
    parser.add_argument("--ip", action="store", type=ip_address,\
                        required=True,\
                        help="IP address of interest, e.g. 0.0.0.0")

    args = parser.parse_args()

    return args

if __name__ == '__main__':
    args = parse_cli_args()
    print(args.ip)

which works as expected, e.g.:

python3 test.py --ip 192.168.1.1

192.168.1.1

python3 test.py --ip derp

usage: test.py [-h] --ip IP test.py: error: argument --ip: invalid ip_address value: 'derp'

python3 test.py --ip

usage: test.py [-h] --ip IP test.py: error: argument --ip: expected one argument

How can I mock these three conditions in unit tests?

I tried a few variations of this:

import unittest
from unittest.mock import patch


class ParseCLIArgs(unittest.TestCase):

    """Unit tests."""

    @patch('builtins.input', return_value='192.168.1.1')
    def test_parse_cli_args_01(self, input):
        """Valid return value."""
        self.assertIsInstance(parse_cli_args(), ipaddress.IPv4Address)

if __name__ == '__main__':
    unittest.main()

without success. What am I doing wrong, and how can I fix that?

EDIT I got a bit further with this:

class ParseCLIArgs(unittest.TestCase):

def setUp(self):
    self.parser = parse_cli_args()

def test_parser_cli_args(self):
    parsed = self.parser.parse_args(['--ip', '192.168.1.1'])
    self.assertIs(parsed.ip, '192.168.1.1')

if __name__ == '__main__':
    unittest.main()

Which fails with: TypeError: isinstance() arg 2 must be a type or tuple of types. I believe this is because the function actually transforms user input.

marshki
  • 175
  • 1
  • 11
  • Without reading your question in detail, I'll make general observation. Test frame works often have their own parser and commandline arguments. So adding your own arguments on top of that can be tricky. I don't recommend including `argparse` in the testing framework. As for your question, it is incomplete. "without success" is an inadequate description of your problem(s). – hpaulj Dec 10 '20 at 18:53
  • I want to write a test that mocks valid user input, e.g. `--ip 192.168.1.1`. The test provided fails with this error: `usage: test2.py [-h] --ip IP test2.py: error: the following arguments are required: --ip`, indicating that I am not actually passing along the mock correctly. – marshki Dec 11 '20 at 14:20

2 Answers2

0

To test a parser you need to either modify sys.argv or provide your own substitute.

When called with

 args = parser.parse_args(argv)

if argv is None (or not provided) it parses sys.argv[1:]. This is the list that the shell/interpreter gives it. Otherwise it will parse the equivalent list that you provide.

test_argparse.py uses both ways to test a parser - constructing a custom sys.argv and call parse_args with a custom argv.

Another thing to watch out for is error trapping. Most parsing errors display usage, error message and then exits. Capturing that exit and the stderr message takes some work. test_argparse makes a subclass of ArgumentParser with a custom error method.

In sum, doing unittest on code that depends on sys.argv and does a system exit, may require more work than it's worth. That said, I'm not an expert on unit testing and mocking tools; I've just studied the test_argparse file.

hpaulj
  • 221,503
  • 14
  • 230
  • 353
  • It isn't clear to me how to read the `test_argparse` file. How can I view that? – marshki Dec 13 '20 at 15:52
  • I used to find in a `test` directory of a development distribution. It probably can be found on the current `github` repository, but I haven't look in a while. It is a complicated file, so I'm not sure it's worth your while to study. Have you searched for previous SO with your tags? – hpaulj Dec 13 '20 at 16:40
  • I did look at a [similar post](https://stackoverflow.com/questions/18160078/how-do-you-write-tests-for-the-argparse-portion-of-a-python-module), though I think my use case is slightly different in that I'm calling another module to vouch for the veracity of user input. I'm also on the *enthusiast* side of the SO spectrum... – marshki Dec 13 '20 at 16:57
  • The answers in the linked SO seem to cover the main issues. – hpaulj Dec 13 '20 at 17:06
0

Hopefully this can serve as good sample for someone with a similar question. This test does most (but not all) of what I want it to do. I'm still working out a unit test to check for Type.

#!/usr/bin/env python3

"""IPv4 validation using `ipaddress module` and argparse."""

import argparse
from ipaddress import ip_address

import unittest

def parse_cli_args():
    """
    Command line parser for subnet of interest.
    Args:
      --ip 0.0.0.0
    Returns:
      String, e.g. 0.0.0.0
    """

    parser = argparse.ArgumentParser(description="IPv4 address of interest.")
    parser.add_argument("--ip", action="store",\
                        required=True,\
                        help="IP address of interest, e.g. 0.0.0.0")

    return parser

class ParseCLIArgs(unittest.TestCase):

    def setUp(self):
        self.parser = parse_cli_args()

    def test_parser_cli_args(self):
        parsed = self.parser.parse_args(['--ip', '192.168.1.1'])
        self.assertEqual(parsed.ip, '192.168.1.1')



if __name__ == '__main__':
    unittest.main()
marshki
  • 175
  • 1
  • 11