Pythonでunittestを使用してユニットテストを実施する
テスト駆動開発が浸透し、ユニットテストはコードの品質を高めるためにとても重要視されています。
ユニットテストとは何なのかを理解し、Pythonでユニットテストを実施するために、テストフレームワークであるunittestの使い方を解説していきます。
ユニットテスト(単体テスト)とは何か
ユニットテスト(単体テスト)とは、ソフトウェア開発においてアプリケーションを構成する個々のユニットやコンポーネントが期待通りに動作しているかどうかを確認するために行うテストです。
ユニットとはアプリケーションの中でテスト可能な最小の部分のことで、多くの場合それは関数やメソッドといった単位となります。
ユニットテストは、アプリケーションの残りの部分からテスト対象を分離して、一つのユニットだけをテストするように設計されます。
ユニットテストは開発者によって書かれることが多く、コードがその要件を満たし、処理が正しく行われていることを保証するために書かれます。
具体的にはテストケースを用意し、ユニットから出力される値が期待されたものと一致するかをアサーション(満たされるべき値の記述)によってチェックします。
ユニットテストには以下のような特徴があります。
- ユニットテストはアプリケーションの他の部分から分離した状態で、単一のユニットをテストするように設計されます。そのユニットに依存関係があったり外部リソースが必要な場合にはモックやスタブを使用します。
- ユニットテストは継続的インテグレーションや継続的デリバリー(CI/CD)の一部として、簡単かつ迅速に実行できるように組み込まれることが多いです。
- ユニットテストは繰り返し実行することが可能であり、実行される環境に関係なく、実行されるたびに同じ結果を生成する必要があります。
- ユニットテストはテスト対象のユニットが堅牢であり、様々な入力や状況に対応できることを保証する必要があります。そのために全てのシナリオとエッジケースをカバーすることが望ましいです。
ユニットテストを行うメリット
ユニットテストを行うことには以下のようなメリットがあります。
- 問題の早期発見:ユニットテストは開発プロセスの早い段階で不具合やエラーを検出することができます。特にテスト駆動開発を実施している場合には、実際のコード実装の前にテストが書かれるため、開発者は早期に実装の問題を発見することができます。
- 開発期間の短縮:テストコードを書く必要があるため、開発者にとってその分の工数は増えるかもしれません。しかしユニットテストを導入することで、開発者はコードの問題を素早く特定・修正できるようになります。さらに、ユニットテストはリグレッション(後戻り)の発生を防ぐのにも役立ち、総合的には開発のスピードアップが期待できます。
- コードの保守性を高める:ユニットテストを意識するとアプリケーションをより小さなコンポーネントに分解することができます。ユニットテストは各コンポーネントが適切に機能していることを確認するのに役立ち、長期的にアプリケーションを保守することが容易になります。
Pythonでユニットテストを行うには
では、Pythonでユニットテストを行う方法を解説します。
Pythonには「unittest」という組み込みテストフレームワークが存在します。unittestはコードの機能をテストし、期待通りに動作することを確認するためのテストディスカバリ、テストフィクスチャ、アサーションといったツール群を提供しています。
unittestはPythonの開発で広く使われており、人気のあるPython IDEと継続的インテグレーションツールでサポートされています。また、pytestやnoseなどの他のテストフレームワークとも互換性があります。
unittestはPythonに標準ライブラリとして組み込まれているため、別途インストールする必要はありません。
ユニットテストの実行クラスにインポートすれば利用可能です。
Pythonでunittestを使用する方法
それでは実際にPythonでunittestを使用してみましょう。
unittestの基本的な記述方法
Pythonにおけるunittestの基本的な記述方法についてです。
ここではmy_module内のmy_functionという関数をテストすると仮定しています。
まず、unittestモジュールとテスト対象としたいモジュールをインポートします。
import unittest
from my_module import my_function
続いてunittest.TestCase クラスを継承したテストクラスを作成します。
このクラスには機能をチェックするためのテストメソッドが含まれます。
テストメソッドは、"test" という単語で始まる必要があります。
アサーションを使用すると、コードの出力が期待通りのものであるかどうかを確認することができます。assertEqual() メソッドは、2 つの値が等しいかどうかを調べるために使用します。
class MyTest(unittest.TestCase):
def test_my_function(self):
result = my_function(2)
self.assertEqual(result, 4)
unittest.main() メソッドを使用してテストファイルを実行します。これにより、テストクラス内のすべてのテストが実行されます。
if __name__ == '__main__':
unittest.main()
ここまでの内容をまとめると以下の形になります。
import unittest
from my_module import my_function
class MyTest(unittest.TestCase):
def test_my_function(self):
result = my_function(2)
self.assertEqual(result, 4)
if __name__ == '__main__':
unittest.main()
テストは実行すると成功したか失敗したかを示す出力が表示されます。テストが失敗した場合は、そのエラーメッセージから問題をデバッグすることになります。
Pythonにおけるunittestの実装例
もう少し具体的にunittestを使用する例を示します。
import unittest
# テスト対象のコード
def add(a, b):
return a + b
# テストクラス
class TestAddFunction(unittest.TestCase):
def test_add_integers(self):
result = add(2, 3)
self.assertEqual(result, 5)
def test_add_floats(self):
result = add(2.5, 3.5)
self.assertAlmostEqual(result, 6.0, places=2)
def test_add_strings(self):
result = add("Hello, ", "world!")
self.assertEqual(result, "Hello, world!")
if __name__ == '__main__':
unittest.main()
この例では、まず2つの数値を足したり、2 つの文字列を連結したりする単純な add 関数を作成しました。そしてunittest.TestCaseを継承したテストクラスTestAddFunctionを作成し、 test_add_integers, test_add_floats, test_add_strings という三つのテストメソッドを定義しています。
各テストメソッドでは異なる引数でadd関数を呼び出し、unittestが提供するassertメソッドを使って、結果が期待通りのものであることをチェックしています。
assertEqual() メソッドは2つの値が等しいことをチェックし、 assertAlmostEqual() メソッドは2つの浮動小数点値がほぼ等しいことをチェックしています。
最後にunittest.main()を呼び出すことでテストを実行します。
このコードを実行すると、下記のような出力が得られるはずです。
これは3件のテストを実行し、それらの結果がOKであったことを示しています。
...
----------------------------------------------------------------------
Ran 3 tests in 0.000s
OK
試しにadd関数を以下のように書き換えて実行してみましょう。
def add(a, b):
return a + b + 1
下記のような出力になります。
FFE
======================================================================
ERROR: test_add_strings (__main__.TestAddFunction)
----------------------------------------------------------------------
Traceback (most recent call last):
File "Main.py", line 22, in test_add_strings
result = add("Hello, ", "world!")
File "Main.py", line 8, in add
return a + b + 1
TypeError: can only concatenate str (not "int") to str
======================================================================
FAIL: test_add_floats (__main__.TestAddFunction)
----------------------------------------------------------------------
Traceback (most recent call last):
File "Main.py", line 19, in test_add_floats
self.assertAlmostEqual(result, 6.0, places=2)
AssertionError: 7.0 != 6.0 within 2 places (1.0 difference)
======================================================================
FAIL: test_add_integers (__main__.TestAddFunction)
----------------------------------------------------------------------
Traceback (most recent call last):
File "Main.py", line 15, in test_add_integers
self.assertEqual(result, 5)
AssertionError: 6 != 5
----------------------------------------------------------------------
Ran 3 tests in 0.000s
FAILED (failures=2, errors=1)
テストが失敗しました。FAILはadd関数からの返り値がアサーションで期待していたものと異なることを示しています。
test_add_strings()については文字列と数値を連結しようとしているため処理自体がERRORとなりました。
setUp()やtearDown()でテスト前後の処理を書く
unittestではsetUp()を使用することでテスト実施前の処理を、tearDown()を使用することでテスト後の処理をそれぞれ記述することができます。
import unittest
# テスト対象のコード
def add(a, b):
return a + b
# テストクラス
class TestAddFunction(unittest.TestCase):
def setUp(self):
print("setUP() called.")
def test_add_integers(self):
print("test_add_integers() called.")
result = add(2, 3)
self.assertEqual(result, 5)
def test_add_floats(self):
print("test_add_floats() called.")
result = add(2.5, 3.5)
self.assertAlmostEqual(result, 6.0, places=2)
def test_add_strings(self):
print("test_add_strings() called.")
result = add("Hello, ", "world!")
self.assertEqual(result, "Hello, world!")
def tearDown(self):
print("tearDown() called.")
if __name__ == '__main__':
unittest.main()
このコードを実行するとテストが成功し、下記の出力が得られます。
setUP() called.
test_add_floats() called.
tearDown() called.
setUP() called.
test_add_integers() called.
tearDown() called.
setUP() called.
test_add_strings() called.
tearDown() called.
ここで覚えておいていただきたいのは、setUp()とtearDown()は各テストメソッドが実行されるタイミングで実行されているということです。そのため、それぞれ3回ずつ呼び出されています。
まとめ
テスト駆動開発が浸透し、ユニットテストを実施してコードの品質を高めることが非常に重要視されています。
コードに存在する問題に早い段階で気づくことができ、コンポーネントに分解することでコードの保守性を高めることにもつながります。
Pythonでユニットテストを実施するライブラリとして、標準で組み込まれているunittestが人気です。
基本的にはアサーションを活用して、メソッドから期待した出力を得られるかを確認していきます。しかし実際にはより複雑なテストを実施したり、エッジケースと呼ばれる少し極端な入力をテストするなどしてコードの品質を高めていく必要があります。
また、ユニットテストではカバレッジも重要となります。外部ライブラリである「coverage」を組み合わせることで、テストのカバレッジを確認することも可能です。