2

これはここやプログラマーのスタックエクスチェンジに最適なのかどうか分かりませんが、最初にここで試してみて、それが適切でない場合はクロスポストします。クリックベースのCLIアプリケーションの単体テストでコンポーネントを模擬する方法は?

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

CLIアプリケーションの作成に役立つさまざまなパッケージを調査しましたが、clickを使用して解決しました。私が心配しているのは、徹底的にアプリケーションを構築してtestableにする前に、どうすれば実際にそれをまとめるのか、それをクリックするとどのように役立つのかということです。私も検討してclick's documentation on testingを読んだ

relevant part of the APIと私はシンプルな機能をテストするためにこれを使用するために管理してきた一方で、私が処理するかどうかはわかりません(私のCLIに引数として渡されたときに--version--help作業を検証します)より高度なテストケース

私が今テストしようとしている具体的な例を提供します。私は...アーキテクチャの次の並べ替えを持っている自分のアプリケーションのためにCommunicationServiceを接続し、直接HTTP経由のWebサービスとの通信に関わるすべてのロジックをカプセル化

architecture

を...計画しています。私の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を使用していますし、これは私がこれまで持っているテストの範囲であるために

:私は私がやって1つの方法を見つけたと思う

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 

答えて

2

それ。私はPythonのunittest.mockモジュールを見つけたが、それを周りに遊んだ後、私は次のように終わった。私の「途切れ」モジュールで

、私は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) 

これは一時的にどのような引数などで、本当のロジックを実行しませんが、それを呼び出して何回記録されますMagicMockのインスタンスとCommunicationService.establish_communication方法を、置き換え:私のテストスイートでは、私は、このテストケースを定義します次に、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) 
関連する問題