Pythonパッケージのテストタイプ#

3種類のテスト: 単体テスト、統合テスト、機能テスト#

テストスイートを作成する際に考慮したいテストには、さまざまなタイプがあります:

  1. 単体テスト

  2. 統合テスト

  3. エンドツーエンド (機能テストとも呼ばれる) テスト

それぞれのテストには異なる目的があります。ここでは、簡単な例に取り組みながら、3種類のテストすべてについて学びます。

単体テスト#

単体テストは、個々のコンポーネントやコードの単位を分離してテストし、それらが正しく動作することを確認するものです。単体テストの目的は、ソフトウェアの各部分、通常は関数やメソッドのレベルが、意図したタスクを正しく実行することを検証することです。

単体テストは、パズルの各パーツが壊れていないかどうかを調べることに例えられます。パズルのピースがすべて合わなければ、完成することはありません。同様に、コードを扱う場合、テストは各関数、属性、クラス、メソッドが分離されたときに正しく機能することを保証します。

ユニットテストの例: 2つの数値を足し合わせる関数があるとします。その関数のユニットテストは、2つの数値を与えると正しい合計が返されることを保証します。これは、単一のユニット (関数) を隔離してチェックするものなので、ユニットテストです。

# src/examplePy/numbers.py
def add_numbers(a: float, b: float) -> float:
    """
    Add two numbers together and return the result.

    Parameters
    ----------
    a : float
        The first number to add.
    b : float
        The second number to add.

    Returns
    -------
    float
        The sum of the two numbers.
    """
    return a + b

上記の関数のユニットテスト例。このテストは、 tests/ ディレクトリの pytest コマンドを使って実行します。

# tests/examplePy/test_numbers.py
from examplePy.numbers import add_numbers


def test_add_numbers():
    """Test the add_numbers function."""
    # test with positive numbers
    result = add_numbers(2, 3)
    assert result == 5, f"Expected 5, but got {result}"

    # test with negative numbers
    result2 = add_numbers(-1, 4)
    assert result2 == 3, f"Expected 3, but got {result2}"

    # test with zero
    result3 = add_numbers(0, 5)
    assert result3 == 5, f"Expected 5, but got {result3}"

上記のテストは、数値を加算する 1 つのケースだけをテストするわけではないことに注意してください。代わりに、正の数の追加、負の数の追加、ゼロの追加などの複数のシナリオをテストします。これは、「add_numbers」関数がさまざまな状況で正しく動作することを保証するのに役立ち、防御的なプログラミングについて考える始まりになります。

pytest testing/test_math_utils.py を使用してターミナルからこのテストを実行できます。

パズルのピースがうまく組み合わさったような画像です。パズルのピースは紫色、緑色、鴨の羽色とカラフルです。

ユニットテストでは、コードの各部分がそれ自体で期待通りに動作することを確認する必要があります。#

統合テスト#

統合テストでは、パッケージの各パーツがどのように連動するか、あるいは統合されるかをテストします。統合テストは、パズルのピースをつなげて全体像を作るようなものです。統合テストは、コードのさまざまな部分がどのように適合し、連携して動作するかに焦点を当てます。

たとえば、温度を変換し、統計を計算する関数があるとします。統合テストでは、温度を変換して分析するワークフロー内でこれらの機能が正しく連携して動作することを確認します。

# src/examplePy/temperature.py
def celsius_to_fahrenheit(celsius: float) -> float:
    """
    Convert temperature from Celsius to Fahrenheit.

    Parameters:
        celsius (float): Temperature in Celsius.

    Returns:
        float: Temperature in Fahrenheit.
    """
    fahrenheit = (celsius * 9 / 5) + 32
    return fahrenheit


def fahrenheit_to_celsius(fahrenheit: float) -> float:
    """
    Convert temperature from Fahrenheit to Celsius.

    Parameters:
        fahrenheit (float): Temperature in Fahrenheit.

    Returns:
        float: Temperature in Celsius.
    """
    celsius = (fahrenheit - 32) * 5 / 9
    return celsius


def average_temperature(temps: list[float]) -> float:
    """
    Calculate average temperature from a list.

    Parameters
    ----------
    temps : list
        List of temperatures.

    Returns
    -------
    float
        Average temperature.
    """
    return sum(temps) / len(temps)


def convert_and_average(temps_celsius: list[float]) -> float:
    """
    Convert list of Celsius temps to Fahrenheit and
    calculate the average.

    Parameters
    ----------
    temps_celsius : list
        List of Celsius temperatures.

    Returns
    -------
    float
        Average temperature in Fahrenheit.
    """
    temps_fahrenheit = [celsius_to_fahrenheit(t)
                        for t in temps_celsius]
    return average_temperature(temps_fahrenheit)

以下は、変換関数と統計関数がどのように連携するかをチェックする統合テストです。

# tests/examplePy/test_temperature.py
from examplePy import temperature


def test_convert_and_average():
    """
    Test that convert_and_average correctly combines conversion
    and averaging.
    """
    # Test with known values: [0, 10, 20] Celsius
    # Should average to 10 Celsius = 50 Fahrenheit
    temps_celsius = [0, 10, 20]
    result = temperature.convert_and_average(temps_celsius)
    assert abs(result - 50.0) < 0.01

    # Test with different values
    temps_celsius = [0, 100]
    result = temperature.convert_and_average(temps_celsius)
    # Average of 32 and 212 Fahrenheit = 122
    assert abs(result - 122.0) < 0.01

この統合テストでは、変換関数と平均化関数が実際のワークフローで期待どおりに連携して動作することを検証します。

パズルのピースが2つあり、一部欠けている画像です。パズルのピースは紫・鴨の羽色・黄色・青色。 それぞれのピースの形が合いません。

パズルのピースの両端が欠けていると、パズルの他の要素と連動することができません。ソフトウェアの個々の関数、メソッド、クラスについても同様です。コードは、ある一連のタスクを実行するために、個々に、そして一緒に働く必要があります。#

パズルのピースがうまく組み合わさったような画像です。パズルのピースは紫色、緑色、鴨の羽色とカラフルです。

統合テストは、コードの一部が連携して動作することが期待されており、期待通りに動作することを保証するものでなければなりません。#

エンドツーエンド(機能)テスト#

Python におけるエンドツーエンドテスト (機能テストとも呼ばれる) は、ソフトウェアの包括的なチェックリストのようなものです。実際のユーザーのワークフローをシミュレートし、コードベースが実際のアプリケーションやユースケースを最初から最後までサポートすることを確認します。これらのテストは、小規模なテストでは見つからないかもしれない問題を検出し、アプリケーション全体が正しく動作することを確認するのに役立ちます。ソフトウェアを実用化する前に最終チェックを行い、スムーズなユーザー体験を提供する準備ができていることを確認する方法だと考えてください。

デイジーを描いたパズルの完成イメージ

エンドツーエンドテストまたは機能テストは、パッケージがサポートするワークフロー全体を表します。#

エンドツーエンドのテスト例: パッケージを開いて温度データを摂氏から華氏に処理/変換し、平均温度を計算するとします。エンドツーエンドのテストでは、このワークフロー全体をシミュレートし、パッケージが入力温度データを正しく処理し、要約平均値を返すことを確認します。エンドツーエンドのテストでは、サンプル データを提供し、ワークフロー全体を実行して、最終出力が正しいことを確認します。

# tests/examplePy/test_temperature.py
def test_temperature_workflow():
    """
    Test the complete temperature processing workflow.

    This end-to-end test provides sample temperature data in
    Celsius, processes it through the full workflow
    (conversion and averaging), and verifies the output is
    correct.
    """
    # Sample temperature data in Celsius
    temps_celsius = [0, 10, 20]

    # Run the complete workflow
    result = temperature.convert_and_average(temps_celsius)

    # Verify the output
    # Average of 32, 50, and 68 Fahrenheit = 50 Fahrenheit
    assert abs(result - 50.0) < 0.01

このエンドツーエンドのテストでは、サンプル データの提供、変換と平均化、出力が正しいことの確認というユーザー ワークフロー全体を実行します。

エンドツーエンドテストは、プログラムが最初から最後までどのように実行されるかも検証します。ドキュメントに追加して CI で実行するチュートリアルも、エンドツーエンドテストのもう一つの例です。たとえば、完全なユーザーワークフローを示すコードが埋め込まれた Jupyter (.ipynb) ノートブックや .md ファイルなどです。

注釈

科学的なパッケージの場合、パッケージがサポートする中核的なワークフローを強調する短いチュートリアルを作成し、ドキュメントのビルド時に実行することで、エンドツーエンドテストを兼ねることができます。

どのテストタイプをいつ使用するか#

テストが初めての場合は、単体テストから始めてください。これらは最も簡単に作成でき、最も速く実行でき、デバッグも簡単です。パッケージが成長するにつれて、最も価値が高まる統合テストとエンドツーエンドのテストを追加できます。

単体テストを作成することから始めます#

単一の関数、メソッド、またはクラスを分離してテストしていますか?

→ はい: 単体テスト を作成します。

  • 例: add_numbers(2, 3) が 5 を返すことを確認します。

  • 単体テストはコードの他の部分に依存しません

  • これらのテストはテスト スイートの基礎を形成します

  • 何かが壊れた場合、単体テストによりどこが壊れているかを簡単に見つけることができます。

次に統合テストを追加する#

複数のコンポーネントがどのように連携して動作するかをテストしていますか?

→ はい: 統合テスト と書き込みます。

  • 例: 温度を変換し、その平均を計算する

  • 統合テストは、個々の部分がすでに機能していることを前提としています

  • これらのテストは、コンポーネントが正しく相互作用することを検証します。

コアワークフローにエンドツーエンドテストを使用する#

完全で現実的なユーザー ワークフローを最初から最後までテストしていますか?

→ はい: エンドツーエンド テスト を使用します。

  • 例: ユーザーが従うであろう完全なデータ処理ワークフローを実行する

  • これらのテストは多くの場合、ドキュメント内の例を反映しています。

  • 最も重要なワークフローには慎重に使用してください。

  • ドキュメントのビルド中に実行されるチュートリアルは、エンドツーエンドのテストとして機能します。

単体テスト、統合テスト、エンドツーエンドテストの比較#

単体テスト、統合テスト、エンドツーエンドテストには、それぞれ相補的な長所と短所があります。単体テストは、そのきめ細かな性質から、どこでエラーが発生しているかを切り分けるのに適しています。しかし、単体テストは、コードの異なるセクションが連携して動作することを検証するのには役に立ちません。

統合テストとエンドツーエンド テストは、プログラムのさまざまな部分が連携して動作することを検証しますが、エラーが発生している場所を正確に即座に特定することにはあまり価値がありません。

テストは完璧である必要はない#

重要なのは、さまざまな種類のテストにまつわる詳細について心配することにエネルギーを費やす必要はないということです。テストスイートの作成に取りかかるときには、パッケージが何をするのか、そしてパッケージの一部をどのようにテストする必要があるのかを考えてください。さまざまな種類のテストに慣れ親しむことは、テストの書き方や、さまざまな種類のテストが互いにどのように補完し合えるかを考える際のフレームワークとなります。

次のステップ#

テストの種類を理解したので、パッケージの 効果的なテストを作成する 方法を学びましょう。次に、ローカルでテストを実行する および 継続的インテグレーション で行う方法を調べます。 CodeCov などのツールを使用してテスト カバレッジの追跡について学ぶこともできます。