Search code examples
pythoncommand-line-interfacepython-unittestpython-click

Testing Python Click Command Exceptions


I am trying to test the raising of exceptions by a command implemented with the Click package.

This is my command:

@click.option(
    '--bucket_name',
...)
@click.option(
    '--group_id',
...)
@click.option(
    '--artifact_id',
...)
@click.option(
    '--version',
...)
@click.option(
    '--artifact_dir',
    required=False,
    default='downloads/artifacts/',
...)
@click.command()
def download_artifacts(
    bucket_name,
    group_id, artifact_id, version,
    artifact_dir
):
    logger.info(
        f"bucket_name: {bucket_name}, "
        f"group_id: {group_id}, "
        f"artifact_id: {artifact_id}, "
        f"version: {version}, "
        f"artifact_dir: {artifact_dir}, "
        )

    if not artifact_dir.endswith('/'):
        raise ValueError(
            "Enter artifact_dir ending with '/' ! artifact_dir: "
            f"{artifact_dir}")
...

This is my test code with assertRaises that doesn't work:

def test_download_artifacts_invalid_dir(
        self,
    ):
        runner = CliRunner()
        with self.assertRaises(ValueError):
            result = runner.invoke(
                download_artifacts,
                '--bucket_name my_bucket \
                --group_id gi \
                --artifact_id ai \
                --version 1.0.0 \
                --artifact_dir artifact_dir'.split(),
                input='5')

The assert fails and it gives E AssertionError: ValueError not raised instead.

I have found this way of testing, which passes, but it doesn't seem very elegant:

def test_download_artifacts_invalid_dir(
        self,
    ):
        runner = CliRunner()
        result = runner.invoke(
            download_artifacts,
            '--bucket_name my_bucket \
            --group_id gi \
            --artifact_id ai \
            --version 1.0.0 \
            --artifact_dir artifact_dir'.split(),
            input='5')
        print(f"result.exception: {result.exception}")
        assert "Enter artifact_dir ending" in str(result.exception)

Solution

  • Two ways to test for exceptions with click.CliRunner()

    The first method is hinted out in the DOCS:

    Basic Testing

    The basic functionality for testing Click applications is the CliRunner which can invoke commands as command line scripts. The CliRunner.invoke() method runs the command line script in isolation and captures the output as both bytes and binary data.

    The return value is a [Result] object, which has the captured output data, exit code, and optional exception attached.

    result = runner.invoke(throw_value_error)
    assert isinstance(result.exception, ValueError)
    

    The second method is to set the catch_exceptions=False parameter on CliRunner.invoke()

    runner.invoke(..., catch_exceptions=False)
    

    Test Code

    import click.testing
    import pytest
    
    @click.command()
    def throw_value_error():
        raise ValueError("This is My Message!")
    
    def test_catch_value_error():
        """Read the CliRunner exception report"""
        runner = click.testing.CliRunner()
        result = runner.invoke(throw_value_error)
        assert isinstance(result.exception, ValueError)
        assert 'My Message' in str(result.exception)
    
    def test_throw_value_error():
        """Have the CliRunner not catch my exception"""
        runner = click.testing.CliRunner()
        with pytest.raises(ValueError):
            runner.invoke(throw_value_error, catch_exceptions=False)
    

    Test Results

    ============================= test session starts ==============================
    platform linux -- Python 3.7.7, pytest-6.2.1 -- /usr/bin/python
    collecting ... collected 2 item
    
    tests/test_api_authz.py::test_catch_value_error PASSED                   [ 50%]
    tests/test_api_authz.py::test_throw_value_error PASSED                   [100%]
    
    ============================== 2 passed in 0.05s ===============================