Skip to content

Latest commit

 

History

History
140 lines (97 loc) · 4.35 KB

File metadata and controls

140 lines (97 loc) · 4.35 KB

Tutorial

Let's assume you use pytest for running your tests, which is certainly a good idea. Your CLI program is called foobar.

You have prepared a pyproject.toml file with a CLI entrypoint. For the tests you have prepared a tests/ folder (outside of foobar/, because you don't want your tests to be packaged up with your application code).

Then your directory layout looks somewhat like one of our CLI examples.

Note

You can easily generate a CLI project of your own from one of our CLI examples using Copier, e.g.

$ copier copy gl:painless-software/cicd/app/cli my-cli

Functional tests

Start with a simple set of functional tests:

  • Is the entrypoint script installed? (tests the configuration in your pyproject.toml file)
  • Can this package be run as a Python module? (i.e. without having to be installed)
  • Is command XYZ available? etc. Cover your entire CLI usage here!

This is almost a stupid exercise: Run the command as a :func:`~cli_test_helpers.shell` command and inspect the exit code of the exiting process, e.g.

def test_runas_module():
    """Can this package be run as a Python module?"""
    result = shell('python -m foobar --help')
    assert result.exit_code == 0
def test_entrypoint():
    """Is entrypoint script installed? (pyproject.toml)"""
    result = shell('foobar --help')
    assert result.exit_code == 0

The trick is that you run a non-destructive command, e.g. by using the usual --help option of every command. This should cover your entire CLI user interface definition.

See more example code.

Unit tests

Then you're ready to take advantage of our helpers.

ArgvContext

:class:`~cli_test_helpers.ArgvContext` allows you to mimic the use of specific CLI arguments:

def test_get_action():
    """Is action argument (get/set) available?"""
    with ArgvContext('foobar', 'get'):
        args = foobar.cli.parse_arguments()

    assert args.action == 'get'

If you don't have argument parsing in a dedicated function you can combine this approach with mocking a target function, e.g.

@patch('foobar.command.baz')
def test_cli_command(mock_command):
    """Is the correct code called when invoked via the CLI?"""
    with ArgvContext('foobar', 'baz'), pytest.raises(SystemExit):
        foobar.cli.main()

    assert mock_command.called

See more example code.

EnvironContext

:class:`~cli_test_helpers.EnvironContext` allows you to mimic the presence (or absence) of environment variables:

def test_fail_without_secret():
    """Must fail without a ``SECRET`` env variable specified"""
    message_regex = "Environment value SECRET not set."

    with EnvironContext(SECRET=None):
        with pytest.raises(SystemExit, match=message_regex):
            foobar.command.baz()
            pytest.fail("CLI doesn't abort with missing SECRET")

See more example code.

RandomDirectoryContext

:class:`~cli_test_helpers.RandomDirectoryContext` allows you to verify that your CLI program logic is independent of where it is executed in the file system:

def test_load_configfile():
    """Must not fail when executed anywhere in the filesystem."""
    with ArgvContext('foobar', 'load'), RandomDirectoryContext():
        foobar.cli.main()