演習

完了

この演習では、Pytest を使用して関数をテストします。 次に、テストの失敗を引き起こす関数におけるいくつかの潜在的な問題を見つけて修正します。 失敗を確認し、Pytest のリッチなエラー報告を使用することは、運用コード内の問題のあるテストやバグを特定して修正するために不可欠です。

この演習では、システム コマンドを入力として受け取り、オプションとして admin_command() ツールでプレフィックスを付ける sudo という関数を使用します。 この関数には、テストを記述して検出するバグがあります。

ステップ 1 - この演習用のテスト ファイルを追加する

  1. テスト ファイルに Python のファイル名前付け規則を使用して、新しいテストファイルを作成します。 テストファイルに test_exercise.py という名前を付け、次のコードを追加します。

    def admin_command(command, sudo=True):
        """
        Prefix a command with `sudo` unless it is explicitly not needed. Expects
        `command` to be a list.
        """
        if sudo:
            ["sudo"] + command
        return command
    

    関数 admin_command() は引数 command を使用してリストを入力として受け取り、必要に応じて sudo を使用してリストにプレフィックスを付けます。 キーワード引数 sudoFalse に設定すると、入力と同じコマンドが返されます。

  2. 同じファイルに、関数 admin_command() 用のテストを追加します。 テストでは、サンプル コマンドを返すヘルパー メソッドを使用します。

    class TestAdminCommand:
    
    def command(self):
        return ["ps", "aux"]
    
    def test_no_sudo(self):
        result = admin_command(self.command(), sudo=False)
        assert result == self.command()
    
    def test_sudo(self):
        result = admin_command(self.command(), sudo=True)
        expected = ["sudo"] + self.command()
        assert result == expected
    

注意

実際のコードと同じファイル内にテストを含めるのは一般的ではありません。 わかりやすくするために、この演習の例では、同じファイルに実際のコードがあります。 実際の Python プロジェクトにおいて、テストは、それらがテストしているコードからファイルとディレクトリによって分離されています。

ステップ 2 - テストを実行し、エラーを特定する

これで、テスト ファイルにテスト対象の関数と、その動作を検証するためのいくつかのテストを追加したので、次はテストを実行してエラーを処理します。

  • Python を使ってファイルを実行します。

    $ pytest test_exercise.py
    

    この実行が完了すると、1 つのテストが成功し、1 つが失敗するはずです。失敗の出力は次の出力のようになります。

    =================================== FAILURES ===================================
    __________________________ TestAdminCommand.test_sudo __________________________
    
    self = <test_exercise.TestAdminCommand object at 0x10634c2e0>
    
        def test_sudo(self):
            result = admin_command(self.command(), sudo=True)
            expected = ["sudo"] + self.command()
    >       assert result == expected
    E       AssertionError: assert ['ps', 'aux'] == ['sudo', 'ps', 'aux']
    E         At index 0 diff: 'ps' != 'sudo'
    E         Right contains one more item: 'aux'
    E         Use -v to get the full diff
    
    test_exercise.py:24: AssertionError
    =========================== short test summary info ============================
    FAILED test_exercise.py::TestAdminCommand::test_sudo - AssertionError: assert...
    ========================= 1 failed, 1 passed in 0.04s ==========================
    

    test_sudo() のテストで出力が失敗します。 Pytest では、比較される 2 つのリストについて詳しく説明しています。 この場合、変数 result には sudo コマンドがありません。これはテストで期待されるコマンドです。

ステップ 3 - バグを修正し、テストを成功させる

変更を加える前に、最初にエラーが発生した理由を理解しておく必要があります。 期待どおりになっていない (結果に sudo が含まれていない) ことはわかっていますが、その理由を知る必要があります。

条件 admin_command() が満たされた場合、関数 sudo=True の次のコード行を確認します。

    if sudo:
        ["sudo"] + command

リストの操作は、値を返すために使用されていません。 返されないので、関数は常に sudo なしでコマンドを返します。

  1. 関数 admin_command() を更新して、sudo コマンドの要求時に変更された結果が使用されるように、リスト操作を返します。 更新した関数は次のようになります。

    def admin_command(command, sudo=True):
        """
        Prefix a command with `sudo` unless it is explicitly not needed. Expects
        `command` to be a list.
        """
        if sudo:
            return ["sudo"] + command
        return command
    
  2. Pytest を使用してテストを再実行します。 Pytest で -v フラグを使用して、出力の詳細度を上げてみます。

    $ pytest -v test_exercise.py
    
  3. 次に、出力を確認します。 今回は 2 つのテストが成功するはずです。

    ============================= test session starts ==============================
    Python 3.9.6, pytest-6.2.5, py-1.11.0, pluggy-1.0.0 
    cachedir: .pytest_cache
    rootdir: /private
    collected 2 items
    
    test_exercise.py::TestAdminCommand::test_no_sudo PASSED                  [ 50%]
    test_exercise.py::TestAdminCommand::test_sudo PASSED                     [100%]
    
    ============================== 2 passed in 0.00s ===============================
    

注意

関数では、大文字と小文字の使用が異なるさらに多くの値を使用できるため、それらのバリエーションに対応するためにテストを追加する必要があります。 これにより、今後関数への変更が別の (予期しない) 動作の原因となる可能性を防ぐことができます。

ステップ 4 - テストを使って新しいコードを追加する

前の手順でテストを追加したので、安心して関数にさらに変更を加え、テストでそれらを検証できるはずです。 変更が既存のテストでカバーされていない場合でも、以前の仮定を破っていないと確信できます。

この場合、関数 admin_command() は引数 command が常にリストであることを盲目的に信頼しています。 便利なエラー メッセージを含む例外が発生することを確認して、その点を改善しましょう。

  1. まず、動作をキャプチャするテストを作成します。 関数はまだ更新されていませんが、テスト優先アプローチ ( テスト 駆動開発 または TDD とも呼ばれます) を試してください。

    • が一番上にインポートされるように、pytest ファイルを更新します。 このテストは、次のように pytest フレームワークの内部ヘルパーを使用します。
    import pytest
    
    • 次に、新しいテストをクラスに追加して、例外を確認します。 このテストは、関数に渡された値がリストでない場合に、関数から TypeError が返されるはずであることを想定します。
        def test_non_list_commands(self):
            with pytest.raises(TypeError):
                admin_command("some command", sudo=True)
    
  2. Pytest を使用してテストをもう一度実行します。 すべて合格するはずです:

    ============================= test session starts ==============================
    Python 3.9.6, pytest-6.2.5, py-1.11.0, pluggy-1.0.0
    rootdir: /private/
    collected 3 items
    
    test_exercise.py ...                                                     [100%]
    
    ============================== 3 passed in 0.00s ===============================
    

    テストは TypeError を確認すれば十分ですが、便利なエラー メッセージを含むコードを追加することをお勧めします。

  3. 関数を更新して、役に立つエラー メッセージが含まれた TypeError を明示的に生成します。

    def admin_command(command, sudo=True):
        """
        Prefix a command with `sudo` unless it is explicitly not needed. Expects
        `command` to be a list.
        """
        if not isinstance(command, list):
            raise TypeError(f"was expecting command to be a list, but got a {type(command)}")
        if sudo:
            return ["sudo"] + command
        return command
    
  4. 最後に、エラー メッセージを確認するように test_non_list_commands() メソッドを更新します。

    def test_non_list_commands(self):
        with pytest.raises(TypeError) as error:
            admin_command("some command", sudo=True)
        assert error.value.args[0] == "was expecting command to be a list, but got a <class 'str'>"
    

    更新されたテストでは、すべての例外情報を保持する変数として error が使用されます。 error.value.args を使用すると、例外の引数を調べることができます。 この場合、最初の引数には、テストで確認できるエラー文字列が含まれます。

作業を確認

この時点で、以下を含む test_exercise.py という名前の Python テスト ファイルが存在するはずです。

  • 引数とキーワード引数を受け取る関数 admin_command()
  • 関数 TypeError 内の役に立つエラー メッセージを含む例外 admin_command()
  • ヘルパー メソッド TestAdminCommand() と、関数 command() をチェックする 3 つのテスト メソッドを持つテスト クラス admin_command()

すべてのテストは、ターミナルで実行するときにエラーなしで合格する必要があります。