unittestを使用してPythonで関数のテストケースを作成する方法
著者はCOVID-19救済基金を選択し、 Write forDOnationsプログラムの一環として寄付を受け取りました。
序章
Python標準ライブラリには、Pythonコードのテストを記述して実行するのに役立つunittestモジュールが含まれています。
unittestモジュールを使用して作成されたテストは、プログラムのバグを見つけるのに役立ち、時間の経過とともにコードを変更するときにリグレッションが発生するのを防ぐことができます。 テスト駆動開発に準拠しているチームは、unittestが、作成されたすべてのコードに対応する一連のテストがあることを確認するのに役立つ場合があります。
このチュートリアルでは、Pythonのunittestモジュールを使用して、関数のテストを記述します。
前提条件
このチュートリアルを最大限に活用するには、次のものが必要です。
TestCaseサブクラスの定義
unittestモジュールによって提供される最も重要なクラスの1つは、TestCaseという名前です。 TestCaseは、関数をテストするための一般的な足場を提供します。 例を考えてみましょう:
import unittest
def add_fish_to_aquarium(fish_list):
if len(fish_list) > 10:
raise ValueError("A maximum of 10 fish can be added to the aquarium")
return {"tank_a": fish_list}
class TestAddFishToAquarium(unittest.TestCase):
def test_add_fish_to_aquarium_success(self):
actual = add_fish_to_aquarium(fish_list=["shark", "tuna"])
expected = {"tank_a": ["shark", "tuna"]}
self.assertEqual(actual, expected)
まず、unittestをインポートして、モジュールをコードで使用できるようにします。 次に、テストする関数を定義します。ここではadd_fish_to_aquariumです。
この場合、add_fish_to_aquarium関数は、fish_listという名前の魚のリストを受け入れ、fish_listに10個を超える要素がある場合にエラーを発生させます。 次に、この関数は、水槽の名前"tank_a"を指定されたfish_listにマッピングする辞書を返します。
TestAddFishToAquariumという名前のクラスは、unittest.TestCaseのサブクラスとして定義されています。 test_add_fish_to_aquarium_successという名前のメソッドは、TestAddFishToAquariumで定義されています。 test_add_fish_to_aquarium_successは、特定の入力を使用してadd_fish_to_aquarium関数を呼び出し、実際の戻り値が、返されると予想される値と一致することを確認します。
テストを使用してTestCaseサブクラスを定義したので、そのテストを実行する方法を確認しましょう。
TestCaseの実行
前のセクションでは、TestAddFishToAquariumという名前のTestCaseサブクラスを作成しました。 test_add_fish_to_aquarium.pyファイルと同じディレクトリから、次のコマンドを使用してそのテストを実行してみましょう。
- python -m unittest test_add_fish_to_aquarium.py
unittestという名前のPythonライブラリモジュールをpython -m unittestで呼び出しました。 次に、TestAddFishToAquariumTestCaseを引数として含むファイルへのパスを指定しました。
このコマンドを実行すると、次のような出力が返されます。
Output.
----------------------------------------------------------------------
Ran 1 test in 0.000s
OK
unittestモジュールがテストを実行し、テストがOKを実行したことを通知しました。 出力の最初の行にある単一の.は、合格したテストを表しています。
ノート: TestCase recognizes test methods as any method that begins with test. For example, def test_add_fish_to_aquarium_success(self) is recognized as a test and will be run as such. def example_test(self), conversely, would not be recognized as a test because it does not begin with test. Only methods beginning with test will be run and reported when you run python -m unittest ....
それでは、失敗したテストを試してみましょう。
テストメソッドの次の強調表示された行を変更して、失敗を導入します。
import unittest
def add_fish_to_aquarium(fish_list):
if len(fish_list) > 10:
raise ValueError("A maximum of 10 fish can be added to the aquarium")
return {"tank_a": fish_list}
class TestAddFishToAquarium(unittest.TestCase):
def test_add_fish_to_aquarium_success(self):
actual = add_fish_to_aquarium(fish_list=["shark", "tuna"])
expected = {"tank_a": ["rabbit"]}
self.assertEqual(actual, expected)
add_fish_to_aquariumは、"tank_a"に属する魚のリストに"rabbit"を返さないため、変更されたテストは失敗します。 テストを実行してみましょう。
ここでも、test_add_fish_to_aquarium.pyと同じディレクトリから次のコマンドを実行します。
- python -m unittest test_add_fish_to_aquarium.py
このコマンドを実行すると、次のような出力が返されます。
OutputF
======================================================================
FAIL: test_add_fish_to_aquarium_success (test_add_fish_to_aquarium.TestAddFishToAquarium)
----------------------------------------------------------------------
Traceback (most recent call last):
File "test_add_fish_to_aquarium.py", line 13, in test_add_fish_to_aquarium_success
self.assertEqual(actual, expected)
AssertionError: {'tank_a': ['shark', 'tuna']} != {'tank_a': ['rabbit']}
- {'tank_a': ['shark', 'tuna']}
+ {'tank_a': ['rabbit']}
----------------------------------------------------------------------
Ran 1 test in 0.001s
FAILED (failures=1)
失敗の出力は、テストが失敗したことを示しています。 {'tank_a': ['shark', 'tuna']}の実際の出力は、{'tank_a': ['rabbit']}のtest_add_fish_to_aquarium.pyに追加した(誤った)期待と一致しませんでした。 また、.の代わりに、出力の最初の行にFが含まれていることにも注意してください。 テストに合格すると.文字が出力されますが、unittestが失敗したテストを実行するとFが出力されます。
テストを作成して実行したので、add_fish_to_aquarium関数の別の動作について別のテストを作成してみましょう。
例外を発生させる関数のテスト
unittestは、入力として魚が多すぎる場合にadd_fish_to_aquarium関数がValueError例外を発生させることを確認するのにも役立ちます。 前の例を拡張して、test_add_fish_to_aquarium_exceptionという名前の新しいテストメソッドを追加しましょう。
import unittest
def add_fish_to_aquarium(fish_list):
if len(fish_list) > 10:
raise ValueError("A maximum of 10 fish can be added to the aquarium")
return {"tank_a": fish_list}
class TestAddFishToAquarium(unittest.TestCase):
def test_add_fish_to_aquarium_success(self):
actual = add_fish_to_aquarium(fish_list=["shark", "tuna"])
expected = {"tank_a": ["shark", "tuna"]}
self.assertEqual(actual, expected)
def test_add_fish_to_aquarium_exception(self):
too_many_fish = ["shark"] * 25
with self.assertRaises(ValueError) as exception_context:
add_fish_to_aquarium(fish_list=too_many_fish)
self.assertEqual(
str(exception_context.exception),
"A maximum of 10 fish can be added to the aquarium"
)
新しいテストメソッドtest_add_fish_to_aquarium_exceptionもadd_fish_to_aquarium関数を呼び出しますが、文字列"shark"を含む25要素の長いリストを25回繰り返して呼び出します。
test_add_fish_to_aquarium_exceptionは、TestCaseが提供するwith self.assertRaises(...) コンテキストマネージャーを使用して、add_fish_to_aquariumが入力されたリストを長すぎるとして拒否することを確認します。 self.assertRaisesの最初の引数は、発生すると予想されるExceptionクラス(この場合はValueError)です。 self.assertRaisesコンテキストマネージャーは、exception_contextという名前の変数にバインドされています。 exception_contextのexception属性には、add_fish_to_aquariumが発生させた基になるValueErrorが含まれています。 そのValueErrorでstr()を呼び出してメッセージを取得すると、予期した正しい例外メッセージが返されます。
test_add_fish_to_aquarium.pyと同じディレクトリから、テストを実行してみましょう。
- python -m unittest test_add_fish_to_aquarium.py
このコマンドを実行すると、次のような出力が返されます。
Output..
----------------------------------------------------------------------
Ran 2 tests in 0.000s
OK
特に、add_fish_to_aquariumが例外を発生させなかったか、別の例外を発生させた場合(たとえば、ValueErrorではなくTypeError)、テストは失敗します。
ノート: unittest.TestCase exposes a number of other methods beyond assertEqual and assertRaises that you can use. The full list of assertion methods can be found ドキュメントで, but a selection are included here:
| 方法 | アサーション |
|---|---|
assertEqual(a, b) |
a == b |
assertNotEqual(a, b) |
a != b |
assertTrue(a) |
bool(a) is True |
assertFalse(a) |
bool(a) is False |
assertIsNone(a) |
a is None |
assertIsNotNone(a) |
a is not None |
assertIn(a, b) |
a in b |
assertNotIn(a, b) |
a not in b |
基本的なテストをいくつか作成したので、TestCaseが提供する他のツールを使用して、テストしているコードを利用する方法を見てみましょう。
setUpメソッドを使用したリソースの作成
TestCaseは、setUpメソッドもサポートしており、テストごとにリソースを作成するのに役立ちます。 setUpメソッドは、すべてのテストの前に実行する準備コードの共通セットがある場合に役立ちます。 setUpを使用すると、個々のテストごとに何度も繰り返すのではなく、このすべての準備コードを1か所にまとめることができます。
例を見てみましょう:
import unittest
class FishTank:
def __init__(self):
self.has_water = False
def fill_with_water(self):
self.has_water = True
class TestFishTank(unittest.TestCase):
def setUp(self):
self.fish_tank = FishTank()
def test_fish_tank_empty_by_default(self):
self.assertFalse(self.fish_tank.has_water)
def test_fish_tank_can_be_filled(self):
self.fish_tank.fill_with_water()
self.assertTrue(self.fish_tank.has_water)
test_fish_tank.pyは、FishTankという名前のクラスを定義します。 FishTank.has_waterは最初はFalseに設定されていますが、FishTank.fill_with_water()を呼び出すことでTrueに設定できます。 TestCaseサブクラスTestFishTankは、新しいFishTankインスタンスをインスタンス化し、そのインスタンスをself.fish_tankに割り当てるsetUpという名前のメソッドを定義します。
setUpはすべての個々のテストメソッドの前に実行されるため、test_fish_tank_empty_by_defaultとtest_fish_tank_can_be_filledの両方に対して新しいFishTankインスタンスがインスタンス化されます。 test_fish_tank_empty_by_defaultは、has_waterがFalseとして開始することを確認します。 test_fish_tank_can_be_filledは、fill_with_water()を呼び出した後、has_waterがTrueに設定されていることを確認します。
test_fish_tank.pyと同じディレクトリから、次のコマンドを実行できます。
- python -m unittest test_fish_tank.py
前のコマンドを実行すると、次の出力が表示されます。
Output..
----------------------------------------------------------------------
Ran 2 tests in 0.000s
OK
最終出力は、2つのテストが両方とも合格したことを示しています。
setUpを使用すると、TestCaseサブクラスですべてのテストに対して実行される準備コードを記述できます。
注:実行するTestCaseサブクラスを持つ複数のテストファイルがある場合は、python -m unittest discoverを使用して複数のテストファイルを実行することを検討してください。 詳細については、python -m unittest discover --helpを実行してください。
tearDownメソッドを使用してリソースをクリーンアップする
TestCaseは、tearDownという名前のsetUpメソッドに対応するものをサポートします。 tearDownは、たとえば、データベースへの接続をクリーンアップする必要がある場合や、各テストの完了後にファイルシステムに加えられた変更を行う必要がある場合に役立ちます。 ファイルシステムでtearDownを使用する例を確認します。
import os
import unittest
class AdvancedFishTank:
def __init__(self):
self.fish_tank_file_name = "fish_tank.txt"
default_contents = "shark, tuna"
with open(self.fish_tank_file_name, "w") as f:
f.write(default_contents)
def empty_tank(self):
os.remove(self.fish_tank_file_name)
class TestAdvancedFishTank(unittest.TestCase):
def setUp(self):
self.fish_tank = AdvancedFishTank()
def tearDown(self):
self.fish_tank.empty_tank()
def test_fish_tank_writes_file(self):
with open(self.fish_tank.fish_tank_file_name) as f:
contents = f.read()
self.assertEqual(contents, "shark, tuna")
test_advanced_fish_tank.pyは、AdvancedFishTankという名前のクラスを定義します。 AdvancedFishTankは、fish_tank.txtという名前のファイルを作成し、それに文字列"shark, tuna"を書き込みます。 AdvancedFishTankは、fish_tank.txtファイルを削除するempty_tankメソッドも公開します。 TestAdvancedFishTank TestCaseサブクラスは、setUpメソッドとtearDownメソッドの両方を定義します。
setUpメソッドは、AdvancedFishTankインスタンスを作成し、それをself.fish_tankに割り当てます。 tearDownメソッドは、self.fish_tankでempty_tankメソッドを呼び出します。これにより、各テストメソッドの実行後にfish_tank.txtファイルが確実に削除されます。 このように、各テストは白紙の状態から始まります。 test_fish_tank_writes_fileメソッドは、"shark, tuna"のデフォルトの内容がfish_tank.txtファイルに書き込まれていることを確認します。
test_advanced_fish_tank.pyと同じディレクトリから実行してみましょう。
- python -m unittest test_advanced_fish_tank.py
次の出力を受け取ります。
Output.
----------------------------------------------------------------------
Ran 1 test in 0.000s
OK
tearDownを使用すると、TestCaseサブクラスのすべてのテストに対して実行されるクリーンアップコードを記述できます。
結論
このチュートリアルでは、さまざまなアサーションを使用してTestCaseクラスを記述し、setUpおよびtearDownメソッドを使用して、コマンドラインからテストを実行しました。
unittestモジュールは、このチュートリアルでカバーしなかった追加のクラスとユーティリティを公開します。 ベースラインができたので、ユニットテストモジュールのドキュメントを使用して、他の利用可能なクラスとユーティリティについて詳しく知ることができます。 Djangoプロジェクトにユニットテストを追加する方法にも興味があるかもしれません。