6

これがここに最適なのか、Programmers Stack Exchange に最適なのかはわかりませんが、最初にここで試して、適切でない場合は別の場所にクロスポストします。

私は最近、Web サービスを開発しました。Python ベースのコマンドライン インターフェイスを作成して、より簡単に操作できるようにしようとしています。簡単なスクリプト作成の目的でしばらくの間 Python を使用してきましたが、CLI アプリケーションを含む本格的なパッケージの作成には経験がありません。

CLI アプリの作成に役立つさまざまなパッケージを調査し、clickを使用することにしました。私が懸念しているのは、実際にすべてを組み立てる前にアプリケーションを完全にテストできるように構成する方法と、クリックを使用してそれを支援する方法です。

テストに関するクリックのドキュメントを読み、API の関連部分を調べました。単純な機能のテスト (CLI に引数として渡されたときの検証--versionと動作) にはこれを使用できましたが--help、どうすればよいかわかりません。より高度なテスト ケースを処理します。

私が今テストしようとしているものの具体的な例を提供します。アプリケーションが次のようなアーキテクチャを持つことを計画しています...

建築

...ここで、CommunicationServiceHTTP 経由で Web サービスに接続して直接通信することに関係するすべてのロジックをカプセル化します。私の CLI は、Web サービスのホスト名とポートのデフォルトを提供しますが、明示的なコマンドライン引数、構成ファイルの書き込み、または環境変数の設定によって、ユーザーがこれらをオーバーライドできるようにする必要があります。

@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)

ユーザーが異なるホスト名とポートを指定した場合、デフォルトではなくこれらの設定を使用してControllerインスタンス化することをテストしたいと思います。CommunicationService

これを行う最善の方法は、次のようなものになると思います。

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, ['--hostname', hostname, '--port', str(port)])
    assert result.exit_code == 0
    assert mock_comms.hostname == hostname
    assert mock_comms.port == port

このケースを適切に処理する方法についてアドバイスを得ることができれば、それを取り上げて同じ手法を使用して、CLI の他のすべての部分をテスト可能にすることができるはずです。

価値があるのは、私は現在テストに pytest を使用しており、これは私がこれまでに得たテストの範囲です:

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'])

        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
4

1 に答える 1

7

私はそれを行う1つの方法を見つけたと思います。私は偶然 Python のunittest.mockモジュールに出くわし、少し遊んだ後、次のようになりました。

私の「comms」モジュールでは、次を定義しますCommunicationService

class CommunicationService(object):
    def establish_communication(self, hostname: str, port: int):
        print('Communications service instantiated with {}:{}'.format(hostname, port))

これはプロダクション クラスであり、print ステートメントは最終的に実際の通信ロジックに置き換えられます。

私のメイン モジュールでは、最上位コマンドでこの通信サービスをインスタンス化し、通信を確立しようとします。

def cli(hostname, port):
    comms = CommunicationService()
    comms.establish_communication(hostname, port)

そして、楽しい部分。私のテスト スイートでは、次のテスト ケースを定義します。

def test_user_can_override_hostname_and_port(self, runner):
    hostname = 'mock_hostname'
    port = 12345

    # noinspection PyUnresolvedReferences
    with patch.object(CommunicationService, 'establish_communication', spec=CommunicationService)\
            as mock_establish_comms:
        result = runner.invoke(testcube.cli,
                               ['--hostname', hostname, '--port', str(port), 'mock.enable', 'true'])

    assert result.exit_code == 0
    mock_establish_comms.assert_called_once_with(hostname, port)

CommunicationService.establish_communicationこれにより、一時的にメソッドが のインスタンスに置き換えられます。このインスタンスはMagicMock実際のロジックを実行しませんが、呼び出された回数や引数などを記録します。その後、CLI を呼び出して、に基づいて通信を確立しようとした方法についてアサーションを作成できます。指定されたコマンドライン引数。

主に Java や C# などの静的に型付けされた言語で書かれたプロジェクトに携わってきたので、既存のプロダクション クラスのパッチ メソッドをモンキーするだけで、それらのクラスのモック バージョンを作成して代用する方法を見つけることができるとは思いもしませんでした。かなり便利です。

CLIがホスト名とポートの明示的なユーザー提供のオーバーライドを無視するように誤って作成した場合...

def cli(hostname, port):
    comms = CommunicationService()
    comms.establish_communication(DEFAULT_SETTINGS['hostname'], DEFAULT_SETTINGS['port'])

...次に、アラートを出すための便利なテスト ケースがあります。

>           raise AssertionError(_error_message()) from cause
E           AssertionError: Expected call: establish_communication('mock_hostname', 12345)
E           Actual call: establish_communication('127.0.0.1', 36364)
于 2016-08-28T13:11:33.760 に答える