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
で呼び出しました。 次に、TestAddFishToAquarium
TestCase
を引数として含むファイルへのパスを指定しました。
このコマンドを実行すると、次のような出力が返されます。
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プロジェクトにユニットテストを追加する方法にも興味があるかもしれません。