AIがPythonコードから自動でUnit Testを生成:PynguinとCodiumAIでCoverageを向上させるガイド

Artificial Intelligence tutorial - IT technology blog
Artificial Intelligence tutorial - IT technology blog

5分で始める:PynguinのインストールとTest自動生成

計算処理を含むいくつかの関数が書かれたPythonファイルがあるのに、まだテストがない——そんな場面を想像してください。「どんなテストケースが必要だろう」と頭を悩ませる前に、AIに任せてしまいましょう。

Pynguinをインストールする:

pip install pynguin

calculator.pyファイルをいくつかのサンプル関数で作成する:

def add(a: int, b: int) -> int:
    return a + b

def divide(a: float, b: float) -> float:
    if b == 0:
        raise ValueError("0で割ることはできません")
    return a / b

def is_palindrome(s: str) -> bool:
    s = s.lower().strip()
    return s == s[::-1]

Pynguinを実行してテストを自動生成する:

pynguin \
  --project-path . \
  --module-name calculator \
  --output-path tests/

数十秒後、Pynguinがtests/test_calculator.pyを生成します。出力例はこのようになります:

import pytest
from calculator import add, divide, is_palindrome

def test_add_0():
    assert add(0, 0) == 0

def test_add_1():
    assert add(1, 2) == 3

def test_divide_0():
    assert divide(4.0, 2.0) == pytest.approx(2.0)

def test_divide_raises():
    with pytest.raises(ValueError):
        divide(1.0, 0.0)

def test_is_palindrome_0():
    assert is_palindrome("racecar") is True

def test_is_palindrome_1():
    assert is_palindrome("hello") is False

テストを実行してcoverageを確認する:

pytest tests/ -v --cov=calculator --cov-report=term-missing

これで完了です。テストがゼロの状態から、数分で80〜90%のcoverageに到達できます。

Pynguinの仕組み

Pynguinはsearch-based software testing(SBST)アルゴリズムを採用しています——具体的には遺伝的アルゴリズムを使ってテストケースを複数世代にわたって「進化」させ、ブランチカバレッジを最大化します。やみくもにランダムなのではなく、できるだけ多くのコードブランチをカバーするという目標に向けて最適化を行います。

実際に使ってみて面白いと感じたのは、Pynguinがエッジケースの発見に優れているという点です——特に例外ブランチの検出が得意です。None、負の数、空文字列、非常に大きなfloatなどを自動的に試して、関数がどう処理するかを確認します。以前のコードでtype checkingに関する隠れたバグがいくつかありましたが、Pynguinが初回実行でそれらをすべて検出してくれました。

重要な前提条件: コードにはtype hintsが必要です。Pynguinはどの型の入力値を生成すべきかを判断するためにtype annotationsに依存しています。a: int-> floatといった型指定のない関数では、生成されるテストが極端に限られるか、まったく生成できない場合があります。

CodiumAI:カバレッジ計測だけでなくロジックを理解するAI

Pynguinはstructural coverageに優れていますが、生成されるテストはやや「機械的」です——test_add_0のような名前や、add(1234567, -9876543)のような意味不明な値が並びます。こうした場面でCodiumAI(現在はQodoに改名)が真価を発揮します。

CodiumAIはLLMを使って関数のビジネスロジックを読み解き、意味のあるテストケースを生成します——テスト名が振る舞いを説明し、より現実的なシナリオをカバーします。

VS CodeでCodiumAIをインストールする

VS Code Marketplaceで「Qodo Gen」(旧名:CodiumAI)を検索してインストールし、無料アカウントを作成します。インストール後、Pythonファイルを開いて任意の関数名をクリックすると——「Generate Tests」ボタンがその関数のすぐ上に表示されます。クリックするとAIが解析を行い、提案されたテストケースが右側のパネルに表示されます。

先ほどのdivide()関数に対して、CodiumAIが生成するテストはこのようになります:

class TestDivide:
    def test_divide_positive_numbers(self):
        """正の数同士の除算——正確な結果が返ること"""
        assert divide(10.0, 2.0) == 5.0

    def test_divide_negative_dividend(self):
        """負の被除数でも正しく計算されること"""
        assert divide(-10.0, 2.0) == -5.0

    def test_divide_by_zero_raises_value_error(self):
        """0除算時にValueErrorが正しいメッセージで送出されること"""
        with pytest.raises(ValueError, match="0で割ることはできません"):
            divide(5.0, 0.0)

    def test_divide_result_precision(self):
        """float型の結果が正確な精度を保つこと"""
        result = divide(7.0, 2.0)
        assert result == pytest.approx(3.5)

同じ関数のテストでも、シナリオを明確に説明する名前があり、docstringがあり、エラーメッセージまで検証しています。コードレビューに出しても、チームメンバーがすぐに読めて説明不要です。

両者を組み合わせる:実践的な戦略

実務を通じて、これは習得すべき重要なスキルの一つだと実感しました——PynguinかCodiumAIかを選ぶのではなく、フェーズに応じて両方を使い分けることが大切です:

  • Pynguin — テストのない既存コードベースのcoverageを素早く確保したいときに使う。一度実行するだけで60〜80%のベースラインが手に入る。
  • CodiumAI — 新機能に使う。意味があり、レビュー可能で、CI/CDに組み込んでも恥ずかしくないテストが必要なときに。

実践的なステップバイステップのワークフロー

# ステップ1:Pynguinでモジュール全体をスキャンする
pynguin \
  --project-path . \
  --module-name myapp.utils \
  --output-path tests/auto/ \
  --maximum-search-time 120

# ステップ2:現在のcoverageを確認する
pytest tests/ --cov=myapp --cov-report=html

# ステップ3:htmlcov/index.htmlを開き、まだ赤いブランチを確認する
# ステップ4:CodiumAIを使って該当ブランチに的を絞ったテストを書く

複雑なコードへのPynguinカスタマイズ

デフォルトでは、Pynguinはモジュールあたり600秒実行されます。ブランチの多いモジュールには、DYNAMOSAアルゴリズムに切り替えましょう:

pynguin \
  --project-path . \
  --module-name myapp.services.payment \
  --output-path tests/auto/ \
  --maximum-search-time 300 \
  --algorithm DYNAMOSA \
  --seed 42

DYNAMOSA(Dynamic Many-Objective Sorting Algorithm)は、モジュールにネストした条件分岐が多い場合により効果的に機能します。

見逃せない実践的なTips

1. Pynguinを実行する前にtype hintsを追加する。 これは必須条件です。アノテーションのない既存コードベースには、mypy --install-typesで自動提案を受けるか、MonkeyTypeを使ってランタイムからtype hintsを自動追加しましょう。

2. AIが生成したテストはcommit前に必ず確認する。 Pynguinはビジネスロジックを理解しません——関数の現在の出力をそのまま記録するだけです。関数にバグがあれば、テストはそのバグを正しいものとしてassertします。以前、テストがすべてpassしていたにもかかわらず割引計算のロジックが誤っていたケースがありましたが、Pynguinは実際の出力が10だったためassert discount == 10を生成していました。

3. 効果を明確に把握するために、前後のcoverageを計測する:

# 適用前
pytest --cov=myapp --cov-report=term | grep TOTAL
# TOTAL  342  289  15%

# AI生成テスト追加後
pytest --cov=myapp --cov-report=term | grep TOTAL
# TOTAL  342   62  82%

4. 外部依存関係を先にモックする。 PynguinもCodiumAIも、データベースやHTTPを呼び出す関数に対しては効果的なテストを生成できません。純粋なロジック部分をI/O部分から分離するか、モックを使いましょう:

from unittest.mock import patch, MagicMock

def test_fetch_user_with_mock():
    with patch("myapp.services.requests.get") as mock_get:
        mock_get.return_value = MagicMock(
            status_code=200,
            json=lambda: {"id": 1, "name": "Test User"}
        )
        result = fetch_user(1)
        assert result["name"] == "Test User"

5. 関数にdocstringがあると、CodiumAIはより良いテストを生成する。 振る舞いと期待されるケースを明確に説明するdocstringがあれば、LLMはコンテキストを理解して、何もない関数よりもずっと適切なシナリオのテストを生成します。

両ツールの真の強みはテストを書くことを代替することではなく——素早く始める手助けをし、基本的な部分のcoverageを確保し、見落としがちなエッジケースを提案することにあります。重要なビジネスロジックに関するテストは、引き続き手動で書いてしっかりとレビューすべきです。

Share: