Testing Commands

Commands can be tested without necessarily doing anything special to work with them in a unit testing context. When a Command runs normally, it does not call sys.exit() on a successful run, but it may do so if an unhandled exception is propagated. Error handling can be disabled, but sometimes you may still need to use with self.assertRaises(SystemExit) in a test case if it is intentional / expected.

Testing Parsing

To ensure that particular combinations of arguments are parsed as expected, the Command.parse() method can be used to parse arguments without running Command.main() or triggering any actions. Parsed attributes may be accessed directly (as they would be from within the Command when running normally), or gathered in a dict via get_parsed.

Using the hello_world.py script that has been used in other examples:

class HelloWorld(Command, description='Simple greeting example', epilog='Contact <example@fake.org> with any issues'):
    name = Option('-n', default='World', help='The person to say hello to')
    count: int = Option('-c', default=1, help='Number of times to repeat the message')

    def main(self):
        for _ in range(self.count):
            print(f'Hello {self.name}!')

We can write these example test cases:

from unittest import TestCase
from cli_command_parser import get_parsed
from hello_world import HelloWorld

class HelloWorldTest(TestCase):
    def test_parse_count(self):
        cmd = HelloWorld.parse(['-c', '5'])
        self.assertEqual(5, cmd.count)

    def test_parse_name_and_count(self):
        cmd = HelloWorld.parse(['-c', '3', '-n', 'Bob'])
        expected = {'name': 'Bob', 'count': 3, 'help': False}
        self.assertDictEqual(expected, get_parsed(cmd))

Test Helpers

A RedirectStreams context manager is available to temporarily override both stdout, stderr, and optionally to provide input for stdin. It’s also possible to use contextlib.redirect_stdout() and/or contextlib.redirect_stderr(), but this does both at once.

To test the result of actually executing the program, the Command.parse_and_run() method can be used. Using the same hello_world.py script as the above test, we can define some additional example test cases that check the output:

from unittest import TestCase
from cli_command_parser.testing import RedirectStreams
from hello_world import HelloWorld

class HelloWorldTest(TestCase):
    def test_hello_default(self):
        with RedirectStreams() as streams:
            HelloWorld.parse_and_run([])
        self.assertEqual('Hello World!\n', streams.stdout)
        self.assertEqual('', streams.stderr)

    def test_hello_test(self):
        with RedirectStreams() as streams:
            HelloWorld.parse_and_run(['-n', 'test'])
        self.assertEqual('Hello test!\n', streams.stdout)

Using InputsExample from the custom_inputs.py script as the example Command being tested, we can see how it works to provide stdin:

def test_custom_input_json_stdin(self):
    with RedirectStreams('{"a": 1, "b": 2}') as streams:
        InputsExample.parse_and_run(['-j', '-'])
    self.assertEqual("You provided a dict\n[0] ('a', 1)\n[1] ('b', 2)\n", streams.stdout)