Webエンジニア向けプログラミング解説動画をYouTubeで配信中!
▶ チャンネル登録はこちら

【ITニュース解説】Writing Unit Tests for Python Code (unittest & doctest)

2025年09月23日に「Dev.to」が公開したITニュース「Writing Unit Tests for Python Code (unittest & doctest)」について初心者にもわかりやすく解説しています。

作成日: 更新日:

ITニュース概要

Pythonのコード品質を保つユニットテスト方法を解説。標準ライブラリのunittestとdoctestを紹介し、主にunittestを用いたテストの書き方、実行、仮想環境でのテスト(モック)を学ぶ。堅牢なシステム開発に必須のスキルが身につく。

ITニュース解説

ソフトウェア開発において、バグのない信頼性の高いシステムを作ることは非常に重要であり、そのためには「テスト」が欠かせない。特に、プログラムの小さな部品(関数やメソッドなど)が正しく動くかを確認する「ユニットテスト」は、開発の初期段階で問題を発見し、手戻りを減らす上で大きな役割を果たす。Pythonには、このユニットテストを助けるための強力なツールが標準ライブラリとして提供されており、その代表的なものがunittestdoctestである。

まず、より包括的なテストフレームワークであるunittestについて見ていこう。unittestは、JavaのJUnitにインスパイアされて作られたもので、テストを体系的に記述し、実行し、結果を報告するための仕組みを提供する。unittestを使う上で理解すべき主要な概念はいくつかある。一つ目の「テストケース」は、テストの最小単位を指し、ある入力に対して期待される出力が得られるかを検証する。二つ目の「テストスイート」は、複数のテストケースや他のテストスイートをまとめたもので、これらを一括で実行できる。三つ目の「テストフィクスチャ」は、テストを実行するための準備(例えば、データベース接続の確立)と、テスト後の後片付け(データベース接続の切断)を行う一連の処理のことである。最後に「テストランナー」は、実際にテストを実行し、その結果をユーザーに伝える役割を担う。

具体的なコード例を見てみよう。例えば、年齢に基づいてカテゴリを分類するcategorize_by_ageという関数が、0から9歳を「Child」、10歳から18歳を「Adolescent」といった具合に分類すると仮定する。この関数が正しく動作するかを検証するために、unittestを使ってテストコードを記述する。テストコードでは、unittest.TestCaseを継承したクラスを作成し、その中にtest_で始まるメソッドを定義する。これらのメソッドが個々のテストケースとなる。例えば、test_childというメソッドでは、categorize_by_age(5)の戻り値が「Child」であるかを確認する。この確認にはself.assertEqual(a, b)のようなアサートメソッドを使う。これは、abが等しいことを検証するメソッドである。同様に、「Adolescent」や「Adult」、さらには無効な年齢に対するテストも記述することで、関数の様々な入力パターンにおける振る舞いを網羅的に検証できる。

テストコードを書き終えたら、次にテストを実行する方法である。最も簡単な方法は、テストファイル自体を直接実行可能にすることだ。テストファイルの末尾にif __name__ == "__main__": unittest.main()という行を追加する。こうすることで、python test_age.pyのようにコマンドラインから直接テストを実行できる。unittest.main()関数には、テスト結果の表示詳細度を調整するverbosity引数を渡すことができる。例えばverbosity=2と設定すると、各テストの名前だけでなく、テストメソッドに記述されたDocstring(説明文)も出力され、テストが何のために行われたのかがより明確になる。この詳細な出力は、多数のテストがある場合にどのテストが実行され、成功したのか、失敗したのかを把握するのに役立つ。

テストの中には、特定の条件下でのみ実行すべきものや、まだ開発中で一時的にスキップしたいものもある。unittestはこのような状況に対応するための機能も提供している。例えば、@unittest.skip("理由")デコレーターを使えば、無条件にそのテストをスキップできる。また、@unittest.skipIf(条件, "理由")を使えば、特定の条件が真の場合にテストをスキップし、@unittest.skipUnless(条件, "理由")を使えば、特定の条件が偽の場合にテストをスキップできる。これにより、例えば特定のPythonバージョンでのみ発生するバグのテストや、特定のOSでのみ必要なテストなどを、環境に応じて柔軟に管理できる。

さらに、unittestの便利な機能として「サブテスト」がある。通常、一つのテストメソッド内で複数の入力値を検証する場合、forループなどを使って繰り返しアサートを行うことがある。しかし、この方法だとループの途中で一つでもアサートが失敗すると、その時点でテストメソッド全体の実行が中断され、残りの入力値に対するテスト結果が得られないという問題がある。ここでsubTestを使うと、ループの各イテレーションが独立したサブテストとして扱われる。これにより、たとえ一つのサブテストが失敗しても他のサブテストは実行され続け、結果としてどの入力値が失敗を引き起こしたのかを明確に把握できる。これは、特に多数の入力値や境界値を検証する際に、デバッグを大幅に効率化する。

unittest.TestCaseクラスには、値の比較だけでなく、様々な検証を行うための豊富なアサートメソッドが用意されている。例えば、assertEqual(a, b)a == bを、assertNotEqual(a, b)a != bを検証する。ブール値の真偽を確認するにはassertTrue(x)assertFalse(x)を用いる。また、オブジェクトの同一性、つまりPythonにおけるオブジェクトのメモリ上のアドレスが同じであるかを確認するためのassertIs(a, b)assertIsNot(a, b)、そしてオブジェクトがNoneであるかを検証するassertIsNone(x)assertIsNotNone(x)もある。さらに、リスト、タプル、辞書、セットといったコレクションの等価性を検証するためのassertListEqual(a, b)assertDictEqual(a, b)といった専用のメソッドも提供されており、これらを適切に利用することで、より表現力豊かで的確なテストコードを記述できる。

複数のテストケースを特定のグループにまとめて実行したい場合は、TestSuiteクラスを利用する。例えば、算術演算のテストと統計演算のテストを別々に実行したい場合、それぞれの操作に関するテストクラスを定義し、それらのテストメソッドを個別にTestSuiteオブジェクトに追加して、TextTestRunnerで実行することができる。これにより、大規模なプロジェクトでもテストの実行を細かく制御し、特定の機能群に絞ってテストを行うことが可能になる。

テストフィクスチャは、テスト環境の準備と後片付けを行うための重要なメカニズムである。unittest.TestCaseクラスには、このための特別なメソッドが用意されている。setUp()メソッドは、各テストメソッドの実行「前」に必ず一度実行される。これは、各テストが必要とする共通のオブジェクトの初期化や状態の設定に利用される。反対にtearDown()メソッドは、各テストメソッドの実行「後」に必ず一度実行される。これは、テスト中に確保したリソースの解放(例えば、開いたファイルのクローズやデータベース接続の切断)など、後片付けのために用いられる。さらに、クラス全体で一度だけ実行されるsetUpClass()tearDownClass()メソッドも存在する。これらは@classmethodデコレーターを付けて定義し、setUpClass()はクラス内の全てのテストが実行される前に一度だけ、tearDownClass()は全てのテストが実行された後に一度だけ実行される。これらのメソッドを適切に利用することで、テスト環境を常にクリーンな状態に保ち、テスト間の依存関係を排除し、テストの信頼性と再現性を高めることができる。

テスト対象のコードが、外部システム(データベース、ネットワークサービス、現在時刻など)や、まだ実装されていない複雑なコンポーネントに依存している場合、それらをそのままテストに含めると、テストの実行が遅くなったり、不安定になったりする。このような場合に役立つのが「モック」である。モックとは、テスト対象が依存するオブジェクトの振る舞いを模擬する偽のオブジェクトのことだ。unittest.mockモジュールは、モックを作成するための強力なツールを提供する。特にMagicMockは、Pythonの特殊メソッド(__len____getitem__など)のデフォルト実装も持っているため、これらのメソッドを使用するオブジェクトをモックする際に非常に便利である。例えば、len(mock_obj)のような操作も、MagicMockを使えば期待通りに機能させることができる。

メソッドをモックする場合、@patch.objectデコレーターを使用する。MyClassgreetというインスタンスメソッドをモックしたい場合、@patch.object(MyClass, 'greet', return_value='Mocked Hello')のように記述する。これにより、テストメソッド内でMyClassのインスタンスからgreetメソッドが呼び出されても、実際には「Mocked Hello」という値が返されるように振る舞いを変更できる。同様に、@classmethodで定義されたクラスメソッドも@patch.object(MyClass, 'get_name', return_value='Mocked Name')のようにモックできる。重要なのは、「パッチする場所は、定義されている場所ではなく、そのモックが使われている場所である」という原則だ。例えば、is_weekdayという関数がdatetime.date.today()を呼び出して現在の日付を取得している場合、@patch("weekday.datetime")のように、is_weekday関数が存在するweekdayモジュール内のdatetimeオブジェクトをモックするべきである。こうすることで、テストの実行中にdatetime.date.today()が常に予測可能な特定の日付(例:2024年4月4日)を返すように設定でき、テストが実行される実際の日付に左右されずに、曜日判定ロジックが正しく機能するかを検証できる。@patchデコレーターは、自動的に作成されたモックオブジェクトをテストメソッドの引数として注入してくれるため、手動でモックオブジェクトを管理する必要がない。これにより、外部依存関係を持つコードも、テストが独立して実行できるように制御でき、信頼性の高いテストを記述することが可能となる。

最後に、doctestについて簡単に触れておこう。doctestは、PythonコードのDocstringやプロジェクトのドキュメント内に記述された例をテストケースとして利用する、軽量なテストフレームワークである。これは、コードの説明とテストを同時に行えるという点で非常にユニークであり、手軽にテストを始めたい場合や、ドキュメントの正確性を保ちたい場合に有効である。

ユニットテストは、ソフトウェアの品質を確保し、変更に強いコードベースを維持するために不可欠なプロセスである。unittestdoctestのようなツールを使いこなすことで、システムエンジニアはより堅牢で信頼性の高いシステムを構築できるようになるだろう。

関連コンテンツ

関連IT用語

関連ITニュース