samedi 27 août 2016

How to mock components within unit tests for Click-based CLI applications?

I'm not sure if this is the best fit for here or the Programmers Stack Exchange, but I'll try here first and cross-post this over there if it's not appropriate.

I've recently developed a web service and I'm trying to create a Python-based command-line interface to make it easier to interact with. I've been using Python for a while for simple scripting purposes but I'm inexperienced at creating full-blown packages, including CLI applications.

I've researched different packages to help with creating CLI apps and I've settled on using click. What I'm concerned about is how to structure my application to make it thoroughly testable before I actually go about putting it all together, and how I can use click to help with that.

I have read click's documentation on testing as well as examined the relevant part of the API and while I've managed to use this for testing simple functionality (verifying --version and --help work when passed as arguments to my CLI), I'm not sure how to handle more advanced test cases.

I'll provide a specific example of what I'm trying to test right now. I'm planning for my application to have the following sort of architecture...

architecture

...where the CommunicationService encapsulates all logic involved in connecting and directly communicating with the web service over HTTP. My CLI provides defaults for the web service hostname and port but should allow users to override these either through explicit command-line arguments, writing config files or setting environment variables:

@click.command(cls=TestCubeCLI, help=__doc__)
@click.option('--hostname', '-h',
              type=click.STRING,
              help='TestCube Web Service hostname (default: {})'.format(DEFAULT_SETTINGS['hostname']))
@click.option('--port', '-p',
              type=click.IntRange(0, 65535),
              help='TestCube Web Service port (default: {})'.format(DEFAULT_SETTINGS['port']))
@click.version_option(version=version.__version__)
def cli(hostname, port):
    click.echo('Connecting to TestCube Web Service @ {}:{}'.format(hostname, port))
    pass


def main():
    cli(default_map=DEFAULT_SETTINGS)

I want to test that if the user specifies different hostnames and ports, then Controller will instantiate a CommunicationService using these settings and not the defaults.

I imagine that the best way to do this would be something along these lines:

def test_cli_uses_specified_hostname_and_port():
    hostname = '0.0.0.0'
    port = 12345
    mock_comms = mock(CommunicationService)
    # Somehow inject `mock_comms` into the application to make it use that instead of 'real' comms service.
    result = runner.invoke(testcube.cli, ['--host', hostname, '--port', str(port)])
    assert result.exit_code == 0
    assert mock_comms.hostname == hostname
    assert mock_comms.port == port

If I can get advice on how to properly handle this case, I should hopefully be able to pick it up and use the same technique for making every other part of my CLI testable.

For what it's worth, I'm currently writing tests in pytest and this is the extent of the tests I've got so far:

import pytest
from click.testing import CliRunner

from testcube import testcube


# noinspection PyShadowingNames
class TestCLI(object):
    @pytest.fixture()
    def runner(self):
        return CliRunner()

    def test_print_version_succeeds(self, runner):
        result = runner.invoke(testcube.cli, ['--version'], foo='bar')

        from testcube import version
        assert result.exit_code == 0
        assert version.__version__ in result.output

    def test_print_help_succeeds(self, runner):
        result = runner.invoke(testcube.cli, ['--help'])
        assert result.exit_code == 0

Aucun commentaire:

Enregistrer un commentaire