ナレッジNEW

Pythonによるテストコードとは?書き方や自動生成について詳しく解説

Pythonによるテストコードとは?書き方や自動生成について詳しく解説

ソフトウェアを実装しデバッグし「テスト(Testing)」するという行為は欠かせません。

本記事では、特にAI適用において広く使われているプログラミング言語 Pythonに着目して、テストコードの基本と実践について分かりやすく解説します。

テストコードの基本を押さえておけば、欠陥を早期に見つけ効率良くデバッグできるようになるため、開発コストの削減や品質確保にもつながります。

また、テストコードを資産化すれば、安心してリファクタリングできるようになり、長期的な保守性も高まります。

テストコードの基本

テストコード(Pythonのテスト用コード)とは、コードの特定の部分が期待通りに正しく動作しているか確認するために書くコードです。

その主な目的は、コードに実装したロジックを検証し、コードをリファクタリング(再構築)したり更新した際に、既存の機能が正しく動作し続けることを保証することです。

テストコードは単にバグを防ぐためだけでなく、コードのドキュメントとしても機能します。テストコードを読み解くことで、そのコードがどのような動作を意図するのかが理解できます。

より具体的にイメージするために、テストレベルの表現方法の一つとしてテストピラミッド(Test Pyramid)を見てみます(図表1、2)。この中のユニットテストにフォーカスして説明します。

図表1:Test Pyramidモデル
図表1:Test Pyramidモデル

The Testing Pyramid: How to Structure Your Test Suite - Semaphoreから引用)

テストピラミッドで表現しているテストレベルには三つあります。

テストレベル

概要

テストの具体例

ユニットテスト(Unit)

関数やメソッドなどの小さな単位で詳細設計通りに動作するかを確認

二つの数値を加算する場合、異なる入力で正しい結果が返るかを確認

統合テスト(Integration)

複数のモジュール間やサービス間の連携によりインターフェースを介して処理や機能が仕様通りに動作するかを確認

外部コンポーネントをモック化せずに、API が正しくデータベースと接続できるかを確認

エンドツーエンドテスト(End to end)

業務要件に基づきシステム全体の機能をエンドツーエンドで確認することを目的とするため、内部の仕組みには立ち入らず、入力と出力を中心に、期待された動作が行われているかを確認

ユーザーが正しくログインでき、適切なデータを閲覧できるかを確認

図表2:テストピラミッドで表現されるテストレベル

テストフレームワークの紹介

テストフレームワークは、テストの作成・整理・実行を支援するツールです。

これらは、繰り返し記述されるボイラープレートコードを避けるのに役立ち、強力なアサート(検証)機能、詳細なエラーレポート、そしてCI/CD(ビルドおよびデプロイの自動化プロセス)との統合機能を提供します。

Pythonでは、最も一般的に使用される二つのフレームワークはpytestとunittestです。では、これらについて見ていきましょう。

pytestとは

pytestは、モダンで柔軟なフレームワークであり、シンプルで拡張しやすい構文が評価され、コミュニティから高く支持されています。主な利点は以下の四つです。

  • シンプルな構文:基本的なテストにはクラスが不要
  • Fixtures:セットアップおよびティアダウンコードを再利用でき、複数のテストで共用できる変数として扱うこともできる
  • Parametrize:一つのテストを複数の入力データセットで実行でき、多くのテスト関数を書くことなく包括的なテストを実現
  • 豊富なプラグイン:カバレッジテストやパラレルテスト(複数のテストケースの同時実行)などを統合できる

それでは、コードのシンプルな例を見てみましょう。

例えば、math_utils.pyファイル内に、二つの数を加算する関数add(a, b)があるとします。 

  def add(a, b): 
      return a + b 

pytest でのテスト (ファイル名は test_math.pyとします) 

import pytest 
from math_utils import add 
 
@pytest.fixture 
def numbers(): 
    return 1, 2  # Fixture再利用 
 
def test_add(numbers): 
    a, b = numbers 
    assert add(a, b) == 3 
 
@pytest.mark.parametrize("a, b, expected", [(1, 2, 3), (0, 0, 0), (-1, 1, 0)]) 
def test_add_parametrized(a, b, expected): 
    assert add(a, b) == expected

 上記では、Fixture numbers を使用してtuple (1, 2) を返し、テスト内で再利用しています。

その後、Parametrize を使用して、複数の入力データセット — ここでは [(1, 2, 3), (0, 0, 0), (-1, 1, 0)] — に対して同じテストが実行できます。

これにより、テストの網羅性が高まり、バグがより簡単に発見できるようになります。

Unittest

Unittestは、Pythonに標準で組み込まれているフレームワークであり、追加のインストールは不要です。

JavaのJUnitを使用したことがある方にはなじみやすく、依存関係を最小限に抑えたい環境としての利用にも適しています。

利点は以下の三つです。

  • 標準ライブラリ(stdlib)に統合されている
  • クラスベースのテストや、setup/teardownメソッドをサポート
  • 大規模なテストスイートに容易に拡張できる

Unittestのコード例を見てみましょう。 

import unittest 
from math_utils import add 
 
class TestAdd(unittest.TestCase): 
    def setUp(self): 
        self.a = 1 
        self.b = 2  # Fixture のようなセットアップ処理 
 
    def test_add(self): 
        self.assertEqual(add(self.a, self.b), 3) 
 
    def test_add_edge_cases(self): 
        self.assertEqual(add(0, 0), 0) 
        self.assertEqual(add(-1, 1), 0) 

Unittestでは、テストクラスが unittest.TestCaseを継承する必要があり、assertEqualなどのメソッドを使用します(図表3)。

図表3:pytest と Unittest の構成イメージ
図表3:pytest と Unittest の構成イメージ

pytest はシンプルで、Unittest は大規模またはレガシープロジェクトに適しています。

テストコードの書き方

テストを分かりやすく、かつ保守しやすくするためには、Arrange–Act–Assert(AAA) という構造に従うことをお勧めします。

この構造に従うことで、テストコードを明確かつ読みやすく整理でき、問題が発生した際にも原因が特定しやすくなります(図表4)。

図表4:AAA構造のイメージ
図表4:AAA構造のイメージ

一つのテストケースは、Arrange(準備)、Act(実行)、Assert(検証) の三つの部分で構成されます。これ以外の工程が一つのテスト内に含まれる場合、テストを分けてシンプルにすることが推奨されています。

(参照:Making Better Unit Tests: part 1, the AAA pattern | by Manning Publications | Medium

この構造を簡単に言い換えると、
「まず何を準備するのか(Arrange)、次に何を行うのか(Act)、そして最終的にどんな結果を期待するのか(Assert)」という流れです。

この捉え方は、テストを理解しやすくするだけでなく、他の開発者や将来の自分がテストの目的を素早く把握する助けにもなります。

AAA構造の例を以下に示します。

def test_add(): 
 # Arrange: データの準備
 a = 2
 b = 3
 # Act: テスト対象の関数を呼び出す
 result = add(a, b)
 # Assert: 結果を検証する
 assert result == 5

テストコードの作成手順

以下ではpytestおよびunittestの両方に共通して適用できる、効果的なテスト作成の基本的な四つの作成手順を紹介します。

1)テストファイルを作成する

ファイル名はtest_で始めます(例:test_math_utils.py)。そしてtests/ディレクトリ内に配置します。この命名規則により、pytestが自動的にテストコードを検出して実行できるようになります。

2)テスト関数を書く

関数名はtest_で始めます(例:test_add_various_cases)。 前記した Arrange–Act–Assert(AAA)の構造に従ってテストコードを記述します。

3)複数のケースを検証し、エッジケースを含める

通常のケース(normal cases)だけでなく、境界ケース(edge cases)──負の数、0、空データなどもテストします。pytest の parametrize や unittest の subTest を使用して、複数の入力を効率的にテスト実装します。

例(pytest):

import pytest 

def add(a, b): 
    return a + b 

@pytest.mark.parametrize("a, b, expected", [ 
    (2, 3, 5),    # Normal case 
    (0, 0, 0),    # Zero case 
    (-1, 1, 0),   # Negative case 
]) 

def test_add_various_cases(a, b, expected):
    assert add(a, b) == expected

4)テストコードを実行し、結果を確認する

まず、Python がインストールされていることを確認してください。ここでは例として Python 3.12.10 を使用しています。

テストコードの実行コマンドは以下の通りです。

■ pytest: pytest test_math_utils.py
■ Unittest: python -m unittest test_math_utils.py

エラーレポートを確認し、問題点を特定・修正します。

例:

さらに発展的なヒント

pytest.raises や assertRaisesRegex を使用して、例外(exception)の発生を確認するテストを行う例を紹介します。

例:

def divide(a, b):
 if b == 0:
  raise ValueError("Cannot divide by zero")
 return a / b
def test_divide_by_zero():
 # Arrange
 a, b = 10, 0
 # Act & Assert
 with pytest.raises(ValueError, match="Cannot divide by zero"):
  divide(a, b)
import unittest
from math_utils import divide

class TestDivide(unittest.TestCase):
    def test_divide_by_zero(self):
        # Arrange 
        a, b = 10, 0
        # Act & Assert
        with self.assertRaisesRegex(ValueError, "Cannot divide by zero"): 
            divide(a, b)

Pythonにおけるモッキング(Mock)

モッキングとは、テスト時に外部のオブジェクトやコンポーネント(例えば API やデータベースなど)を擬似的に再現する技術のことです。

これにより、実際のシステムに依存せずにテスト実行できるため、より高速で安定し、ネットワーク接続などの外部要因に影響されにくくなります。

Pythonでは、標準ライブラリの unittest.mock モジュール、または pytest 用のプラグイン pytest-mock を使用してモックを実装できます。

例:ユーザー情報を取得するために API を呼び出す関数の場合

import requests 
 
def get_user_data(user_id): 
    response = requests.get(f"https://api.example.com/users/{user_id}") 
    return response.json() 

モックでテストを使用すると、次のようになります。

from unittest.mock import patch 
 
@patch('requests.get') 
def test_get_user_data(mock_get): 
    mock_get.return_value.json.return_value = {'name': 'John'} 
    assert get_user_data(1) == {'name': 'John'} 
    mock_get.assert_called_with("https://api.example.com/users/1")
# test_user_service.py 
from unittest.mock import patch 
from user_service import get_user_data 

@patch('requests.get') 
def test_get_user_data(mock_get): 
    # Arrange: モックでダミーデータを返すように設定する 
    mock_get.return_value.json.return_value = {'name': 'John'} 
    # Act: 関数を呼び出す 
    result = get_user_data(1) 
    # Assert: 結果を確認する 
    assert result == {'name': 'John'} 
    mock_get.assert_called_once_with("https://api.example.com/users/1")  

モックを効果的に使うために注意すべきポイント

  • 外部依存となるAPIやデータベースなどはモック化し、内部ロジックはモック化しない
  • モックが正しく呼び出されたかをassert_called_once_with を使用して確認する
  • モックはできるだけシンプルにして、ネストの多い複雑な構造は避ける
  • テストに使うモックデータは、実際に返される実データに近い形にする
  • エラーを意図的に発生させるモックを設定し、例外処理の動作をモックで確認する

テストコードの自動生成

テストコードの自動生成は、時間を節約し、テストのカバレッジ(coverage)を向上させ、人間が見落としやすいケースを検出するのに非常に役立ちます。

特に大規模なプロジェクトにおいては、全てのテストケースを手作業で記述するのは非現実的です。テスト自動生成とテスト分析においてツール活用することで効率的にテストできます。

Pythonでテストコードを自動生成するためのツールと方法

Pythonでテストコードを自動生成するためのツールと利用方法を紹介します。

pytestを用いたテスト分析

pytestは、特別な設定をしなくても test_*.py ファイルや、test_* で始まる関数を自動的に検出します。テストファイルを tests/ ディレクトリ内に配置し、pytest を実行するだけで、test_* で定義されたテストケースが自動的に検出・実行されます。

Hypothesisを用いたプロパティベーステスト(Property-Based Testing)

Hypothesisは、特定の性質(properties)に基づいてランダムなデータを自動生成しテストを行うライブラリであり、インストールは「pip install hypothesis」で実行できます。例えば、add関数の交換法則(commutativity)をテストする場合などに利用します。

from hypothesis import given 
import hypothesis.strategies as st 
from math_utils import add 

@given(st.integers(), st.integers()) 
def test_add_commutative(a, b): 
    assert add(a, b) == add(b, a)

利点として、Hypothesisは何千もの入力データを自動生成してテストするため、負の数や非常に大きな数値といったエッジケース(境界値)を含めたテスト実行ができます。

AI支援によるテストコード作成

GitHub Copilot のようなAIによる支援ツールは、元の関数を基に fixtures や parametrize、さらにはエッジケース(境界値)を含むテストコードを自動的に提案してくれるため、高品質なテストケースを短時間で作成できます。

また、テストコード作成にかかる工数と時間も大幅に削減できます。IDE 上で対象の関数部分を選択し、「Generate pytest for this」と指示するだけで利用できます。

コマンド入力の例を見てみましょう。

ユニットテストのベストプラクティス

Pythonでテストコードを書く際にベストプラクティスを守ることは、高品質なテストを作成するだけでなく、保守性や拡張性の高いコードを書くことにもつながります。

以下にて、初心者でも効果的に実践できる原則と実践方法を一覧表形式にまとめました(図表5)。

原則

概要

理由や補足

独立性(Independent Tests)

各テストケースは独立して、他のテストの結果や状態に依存しないようにする。これにより、テストをどの順番で実行しても結果に影響しないことが保証される。

テスト同士が依存していると、一つの失敗が連鎖的に他のテストに影響し、原因の特定が困難になるため(ドミノ効果)。

高速性(Fast Tests) 

ネットワーク、データベース、システムファイルへのアクセスなど、時間のかかる処理は避けるべき。 

テストが遅いと、実行頻度が下がり、開発時のフィードバックサイクルが遅くなってしまうため。モッキング(mocking)を活用して依存を切り離すのが有効。

結果の一貫性(Deterministic) 

テスト結果は常に同じでなければならず、時間、乱数シードなどのような外部要因に左右されないようにする。

結果に一貫性がないテストは、テストセット全体への信頼を損なうことになるため。

明確なテストケース名

テストの名前は、検証している動作を正確に表すものにする。

Arrange–Act–AssertやGiven–When–Then構造により、テストをドキュメントとしても活用できる。

Flakyテストを避ける

ネットワーク、クロック、並行性などの外部要因によって合格/失敗する、実行結果が不安定(Flaykyとなる)なテストは避ける。 

Flakyテストはテストセット全体の信頼性を低下させかねないため。

テストカバレッジ

コードをユニットテストで実行できた範囲を示す指標。構造をどこまでテストするかで、命令カバレッジや分岐カバレッジなどといった種類がある。

カバレッジを追うことでテスト実行で動いていないコード部分が見つけやすくなる。

テストは短く、一つのテストで一つの動作のみ検証する

一つのテストコードでは一つの特定の振る舞いのみを対象とし、複数の機能を同時に確認することは避ける。

短くシンプルなテストは読みやすく、デバッグが容易になるため。

Fixturesによるコードの再利用

pytestのfixturesやunittestのsetUpを使用することで、同じセットアップ処理を繰り返さないようにする。

同じセットアップ処理を繰り返さないことで、コードの再利用性が向上するため。

エッジケース(境界値)の検証

常に空の入力、負の値、最大値などの境界条件を考慮したテストを行う。

境界条件を考慮することで、予期しない欠陥の作り込みを防止できるため。

図表5:一般的な原則と実践方法の一覧

テストファイルの整理方法

ディレクトリ構造を明確にするよう保つことは、特に大規模プロジェクトにおいて、テストを効果的に管理する上で重要です。

以下は推奨されるディレクトリ構成の一例です。

/project 
  /src 
    __init__.py 
    math_utils.py 
    user_service.py 
 /tests 
    /unit 
      test_math_utils.py 
      test_user_service.py 
    /integration 
      test_api_integration.py 
    /fixtures 
      user_fixtures.py 
    conftest.py  # 共通のFixtures

ヒント: pytest --collect-only でテストの構築が正しく認識されることを確認する

ディレクトリ構成で、ユニットテストと統合テストを分離する理由は次の通りです。

  • ユニットテスト(Unit Tests)の実行は早く、開発中に頻繁に実行するのに適しているため。
  • 統合テスト(Integration Tests)は時間がかかる傾向があるため、CI(自動ビルド環境)やステージング環境などで実行するのが適切であるため。

プロジェクトにおける pytest の設定

pytestを正しく構成することで、テストの自動化と標準化が実現できます。

プロジェクトのルートディレクトリにpytest.iniファイルを作成します。

[pytest] 
addopts = -v --cov=src --cov-report=html --cov-fail-under=80 
testpaths = tests 
python_files = test_*.py 
python_functions = test_* 

説明:

  • -v: テスト実行時に詳細情報を表示する。
  • --cov=src: src ディレクトリ内のコードに対してカバレッジを測定する。
  • --cov-report=html: カバレッジレポートをHTML形式で生成する。
  • --cov-fail-under=80: カバレッジが80%未満の場合、テストを失敗として扱う。
  • testpaths: テストファイルが配置されているディレクトリを指定する。

支援ツールの活用

Pythonテストコードの品質を高めるためには、適切な支援ツールの活用が不可欠です。

これらのツールを利用することで、テストの自動化や保守性の向上、コードの信頼性確保が容易になります。本記事では、テスト自動化を効率化するための支援ツールを三つ紹介します(図表6)。

ツール

説明

pytest-cov

コードカバレッジを追跡し、テストされていないコード部分を特定できる。
・インストール: pip install pytest-cov
・HTMLレポートを出力することで、どの行がカバーされていないかを視覚的に確認できる。

pytest-xdist

テストを並列実行し、処理速度の向上が期待できる。
・インストール: pip install pytest-xdist
・実行例: pytest -n auto

Tox

複数のPythonバージョンでコードをテストするための管理ツール。

図表6:支援ツール

テストコード についてのまとめ

本記事では、Pythonで記述するテストコードの基本的な作成手順について解説しました。

特に初心者の方は、まずは小さな関数に対して一つのシンプルなテストを書くことから始めてみましょう。 軽量なpytestを試してみてもよいですし、従来型のunittestで基本を学ぶのも効果的です。

モッキング(Mocki)を活用して複雑なケースを再現し、pytest-covやpytest-xdistのようなツールでテストの品質・速度を向上させることも次のステップとしてお勧めです。

書くテストの一つ一つが、バグを検出するためだけでなく、より良いエンジニアへと導く一歩になります。

■参考資料■

SNSシェア

この記事は面白かったですか?

今後の改善の参考にさせていただきます!

Search Articles By The Cast出演者/執筆者から記事を探す

Search Articless By The Categoryカテゴリから記事を探す

Ranking

ランキング

もっと見る