●assertEqualとは?
プログラミングを行う上で、コードが意図した通りに動作することを確認するためのテストは非常に重要です。
Pythonの標準ライブラリであるunittestモジュールには、テストを支援する様々な機能が用意されています。
その中でも、2つの値が等しいかどうかを検証するassertEqual関数は、テストコードを書く際に頻繁に使用されます。
assertEqualは、第1引数と第2引数の値を比較し、両者が等しければテストをパスし、等しくなければテストを失敗させます。
この関数を使うことで、期待される結果と実際の結果が一致しているかを簡単にチェックできます。
○assertEqualの基本的な使い方
assertEqualの基本的な使い方は次の通りです。
import unittest
class TestExample(unittest.TestCase):
def test_equal(self):
self.assertEqual(1 + 1, 2)
このコードでは、unittest.TestCaseを継承したTestExampleクラスを定義し、その中でtest_equal関数を定義しています。
test_equal関数内では、assertEqualを使って「1 + 1」の結果が2と等しいかどうかをテストしています。
実行結果
.
----------------------------------------------------------------------
Ran 1 test in 0.001s
OK
テストがパスすると、上記のような出力結果が得られます。
もし等式が成り立たない場合は、次のようなエラーメッセージが表示されます。
F
======================================================================
FAIL: test_equal (__main__.TestExample)
----------------------------------------------------------------------
Traceback (most recent call last):
File "test_example.py", line 5, in test_equal
self.assertEqual(1 + 1, 3)
AssertionError: 2 != 3
----------------------------------------------------------------------
Ran 1 test in 0.001s
FAILED (failures=1)
このように、assertEqualを使うことで期待される結果と実際の結果が一致しているかを簡単に確認できます。
○サンプルコード1:整数値の比較
整数値を比較する場合の使用例を見てみましょう。
import unittest
class TestInteger(unittest.TestCase):
def test_integer_equal(self):
expected = 10
actual = 5 + 5
self.assertEqual(expected, actual)
def test_integer_not_equal(self):
expected = 10
actual = 5 + 6
self.assertEqual(expected, actual)
test_integer_equal関数では、期待される値(expected)と実際の値(actual)が共に10なので、テストはパスします。
一方、test_integer_not_equal関数では、actualが11なのでテストは失敗します。
実行結果
.F
======================================================================
FAIL: test_integer_not_equal (__main__.TestInteger)
----------------------------------------------------------------------
Traceback (most recent call last):
File "test_integer.py", line 11, in test_integer_not_equal
self.assertEqual(expected, actual)
AssertionError: 10 != 11
----------------------------------------------------------------------
Ran 2 tests in 0.001s
FAILED (failures=1)
このように、assertEqualを使えば整数値の比較を簡潔に記述できます。
○サンプルコード2:浮動小数点数の比較
浮動小数点数を比較する場合、丸め誤差によって厳密な等値比較が困難な場合があります。
そのような場合は、assertAlmostEqualを使って大体の値が等しいかどうかを確認するのが一般的です。
import unittest
class TestFloat(unittest.TestCase):
def test_float_equal(self):
expected = 0.1 + 0.2
actual = 0.3
self.assertAlmostEqual(expected, actual)
def test_float_not_equal(self):
expected = 0.1 + 0.2
actual = 0.31
self.assertAlmostEqual(expected, actual, places=2) # 小数点以下2桁まで比較
test_float_equal関数では、0.1 + 0.2と0.3がほぼ等しいと見なされるためテストはパスします。
test_float_not_equal関数では、0.1 + 0.2と0.31の差が小数点以下2桁までの範囲で等しくないと判断されるため、テストは失敗します。
実行結果
.F
======================================================================
FAIL: test_float_not_equal (__main__.TestFloat)
----------------------------------------------------------------------
Traceback (most recent call last):
File "test_float.py", line 12, in test_float_not_equal
self.assertAlmostEqual(expected, actual, places=2)
AssertionError: 0.30000000000000004 != 0.31 within 2 places
----------------------------------------------------------------------
Ran 2 tests in 0.001s
FAILED (failures=1)
assertAlmostEqualを使えば、浮動小数点数の比較を柔軟に行えます。
places引数で指定した桁数までの一致を判定できるので、丸め誤差の影響を抑えられます。
●文字列に対するassertEqual
assertEqualは文字列の比較にも使えて、プログラムで文字列を操作する際のテストに威力を発揮します。
期待される文字列と実際の文字列が一致するかどうかを確認できるので、文字列処理のバグを見つけやすくなりますよ。
文字列比較の場合、assertEqualは文字列の完全一致をチェックします。
つまり、大文字と小文字の違いや、空白文字の有無なども区別されるんです。
○サンプルコード3:文字列の完全一致
では、文字列の完全一致をテストする例を見てみましょう。
import unittest
class TestString(unittest.TestCase):
def test_string_equal(self):
expected = "Hello, World!"
actual = "Hello, " + "World!"
self.assertEqual(expected, actual)
def test_string_not_equal(self):
expected = "Hello, World!"
actual = "hello, world!"
self.assertEqual(expected, actual)
test_string_equal関数では、期待される文字列と実際の文字列が完全に一致しているので、テストはパスします。
一方、test_string_not_equal関数では、大文字と小文字が異なるためテストは失敗します。
実行結果
.F
======================================================================
FAIL: test_string_not_equal (__main__.TestString)
----------------------------------------------------------------------
Traceback (most recent call last):
File "test_string.py", line 11, in test_string_not_equal
self.assertEqual(expected, actual)
AssertionError: 'Hello, World!' != 'hello, world!'
- Hello, World!
? ^ ^
+ hello, world!
? ^ ^
----------------------------------------------------------------------
Ran 2 tests in 0.001s
FAILED (failures=1)
失敗したテストでは、assertionErrorが発生し、期待された文字列と実際の文字列の違いが表示されます。?の位置が違いを示しているので、どこが間違っているのかが一目瞭然ですね。
○サンプルコード4:大文字・小文字の区別
先ほどの例では、大文字と小文字の違いでテストが失敗しましたが、それを無視して比較したい場合もあるでしょう。
そんな時は、assertEqualの代わりにassertEqual.lower()を使います。
import unittest
class TestStringCase(unittest.TestCase):
def test_string_case_insensitive(self):
expected = "Hello, World!"
actual = "hello, world!"
self.assertEqual(expected.lower(), actual.lower())
expected.lower()とactual.lower()で両方の文字列を小文字に変換してから比較しているので、大文字と小文字の違いは無視されます。
これでテストはパスしますね。
実行結果
.
----------------------------------------------------------------------
Ran 1 test in 0.000s
OK
文字列比較の際は、必要に応じてassertEqualとassertEqual.lower()を使い分けると良いでしょう。
完全一致が必要な場合はassertEqualを、大文字と小文字を区別しない場合はassertEqual.lower()を使うのがポイントです。
●リストやタプルのassertEqual
プログラムを書いていると、リストやタプルのような複数の要素を持つデータ構造を扱う機会が多いですよね。
そんな時、assertEqualを使えば、期待されるリストやタプルと実際の結果が一致しているかを簡単にチェックできます。
リストやタプルの比較では、各要素の値と順番が同じであることを確認します。
ただし、リストとタプルは型が異なるため、互いに等しいとは見なされません。
○サンプルコード5:要素の順番が同じ場合
では、要素の値と順番が同じ場合のリストとタプルの比較を見てみましょう。
import unittest
class TestListTuple(unittest.TestCase):
def test_list_equal(self):
expected = [1, 2, 3]
actual = [1, 2, 3]
self.assertEqual(expected, actual)
def test_tuple_equal(self):
expected = (1, 2, 3)
actual = (1, 2, 3)
self.assertEqual(expected, actual)
def test_list_tuple_not_equal(self):
expected = [1, 2, 3]
actual = (1, 2, 3)
self.assertEqual(expected, actual)
test_list_equal関数とtest_tuple_equal関数では、期待される値と実際の値が同じなので、テストはパスします。
一方、test_list_tuple_not_equal関数では、リストとタプルを比較しているため、テストは失敗します。
実行結果
.F
======================================================================
FAIL: test_list_tuple_not_equal (__main__.TestListTuple)
----------------------------------------------------------------------
Traceback (most recent call last):
File "test_list_tuple.py", line 16, in test_list_tuple_not_equal
self.assertEqual(expected, actual)
AssertionError: Lists differ: [1, 2, 3] != (1, 2, 3)
First differing element 0:
1
(1, 2, 3)
- [1, 2, 3]
+ (1, 2, 3)
----------------------------------------------------------------------
Ran 2 tests in 0.001s
FAILED (failures=1)
リストとタプルは異なる型と見なされるため、等価ではないというわけですね。
型が異なるオブジェクト同士の比較には注意が必要です。
○サンプルコード6:要素の順番が異なる場合
次に、リストやタプルの要素の順番が異なる場合を見てみましょう。
import unittest
class TestListOrder(unittest.TestCase):
def test_list_different_order(self):
expected = [1, 2, 3]
actual = [3, 2, 1]
self.assertEqual(expected, actual)
def test_tuple_different_order(self):
expected = (1, 2, 3)
actual = (3, 2, 1)
self.assertEqual(expected, actual)
test_list_different_order関数とtest_tuple_different_order関数では、期待される値と実際の値で要素の順番が異なっているため、両方のテストが失敗します。
実行結果
FF
======================================================================
FAIL: test_list_different_order (__main__.TestListOrder)
----------------------------------------------------------------------
Traceback (most recent call last):
File "test_list_order.py", line 7, in test_list_different_order
self.assertEqual(expected, actual)
AssertionError: Lists differ: [1, 2, 3] != [3, 2, 1]
First differing element 0:
1
3
- [1, 2, 3]
? ^
+ [3, 2, 1]
? ^
======================================================================
FAIL: test_tuple_different_order (__main__.TestListOrder)
----------------------------------------------------------------------
Traceback (most recent call last):
File "test_list_order.py", line 12, in test_tuple_different_order
self.assertEqual(expected, actual)
AssertionError: Tuples differ: (1, 2, 3) != (3, 2, 1)
First differing element 0:
1
3
- (1, 2, 3)
? ^
+ (3, 2, 1)
? ^
----------------------------------------------------------------------
Ran 2 tests in 0.001s
FAILED (failures=2)
リストやタプルの比較では、要素の順番も重要なので、期待される順番と実際の順番が一致していることを確認しましょう。
順番が異なる場合は、assertEqualがその違いを教えてくれます。
●辞書のassertEqual
辞書は、キーと値のペアを格納するデータ構造で、Pythonプログラミングではよく使われています。
辞書に対してもassertEqualを使ってテストを書くことができるんです。
辞書の比較では、キーと値のペアが完全に一致しているかどうかを確認します。
辞書のキーの順序は関係ありません。
○サンプルコード7:キーと値のペアの一致
では、辞書のキーと値のペアが一致する場合のテストを見てみましょう。
import unittest
class TestDict(unittest.TestCase):
def test_dict_equal(self):
expected = {'a': 1, 'b': 2, 'c': 3}
actual = {'a': 1, 'c': 3, 'b': 2}
self.assertEqual(expected, actual)
test_dict_equal関数では、期待される辞書と実際の辞書でキーと値のペアが完全に一致しているので、テストはパスします。
キーの順序が異なっていても、ペアが同じであればassertEqualは等価であると判断します。
実行結果
.
----------------------------------------------------------------------
Ran 1 test in 0.001s
OK
辞書のテストでは、キーと値のペアさえ合っていれば順序は気にしなくて良いというわけですね。
○サンプルコード8:辞書のサブセットの比較
次に、一方の辞書がもう一方の辞書のサブセットになっている場合を見てみましょう。
import unittest
class TestDictSubset(unittest.TestCase):
def test_dict_subset(self):
expected = {'a': 1, 'b': 2}
actual = {'a': 1, 'b': 2, 'c': 3}
self.assertEqual(expected, actual)
test_dict_subset関数では、期待される辞書が実際の辞書のサブセットになっているため、テストは失敗します。
assertEqualは完全な一致を期待するので、片方の辞書に余分なキーと値のペアがあるとアサーションエラーが発生します。
実行結果
F
======================================================================
FAIL: test_dict_subset (__main__.TestDictSubset)
----------------------------------------------------------------------
Traceback (most recent call last):
File "test_dict_subset.py", line 7, in test_dict_subset
self.assertEqual(expected, actual)
AssertionError: {'a': 1, 'b': 2} != {'a': 1, 'b': 2, 'c': 3}
- {'a': 1, 'b': 2}
+ {'a': 1, 'b': 2, 'c': 3}
----------------------------------------------------------------------
Ran 1 test in 0.001s
FAILED (failures=1)
辞書のサブセットを比較したい場合は、assertEqualではなくassertDictEqual(expected, actual)を使うと良いでしょう。
assertDictEqualは、期待される辞書がactualの辞書のサブセットであれば等価と見なします。
●assertEqualのオプション引数
assertEqualは比較の際に便利なオプション引数を提供しているんです。
このオプションを使うと、テストの可読性や保守性を高められます。
○サンプルコード9:エラーメッセージのカスタマイズ
assertEqualのmsg引数を使うと、テスト失敗時に表示されるエラーメッセージをカスタマイズできます。
デフォルトのエラーメッセージでは伝えきれない情報を追加したい時に役立ちますよ。
import unittest
class TestCustomMessage(unittest.TestCase):
def test_custom_message(self):
expected = 42
actual = 13
self.assertEqual(expected, actual, msg="値が期待通りではありません")
test_custom_message関数では、期待される値と実際の値が異なるため、テストは失敗します。
その際、msg引数で指定したカスタムメッセージが表示されるんです。
実行結果
F
======================================================================
FAIL: test_custom_message (__main__.TestCustomMessage)
----------------------------------------------------------------------
Traceback (most recent call last):
File "test_custom_message.py", line 7, in test_custom_message
self.assertEqual(expected, actual, msg="値が期待通りではありません")
AssertionError: 42 != 13 : 値が期待通りではありません
----------------------------------------------------------------------
Ran 1 test in 0.001s
FAILED (failures=1)
エラーメッセージに「値が期待通りではありません」というカスタムメッセージが追加されているのがわかりますね。
テストが失敗した原因をわかりやすく伝えられるので、デバッグがしやすくなります。
○サンプルコード10:テストケースのグルーピング
unittestでは、関連するテストケースをグループ化するためのサブテスト機能があります。
assertEqualと組み合わせることで、テストの構造をわかりやすくできますよ。
import unittest
class TestSubtest(unittest.TestCase):
def test_subtest(self):
cases = [
(1, 1, 2),
(2, 2, 4),
(3, 3, 6),
]
for a, b, expected in cases:
with self.subTest(a=a, b=b, expected=expected):
actual = a + b
self.assertEqual(expected, actual)
test_subtest関数では、複数のテストケースを1つのテスト関数にまとめています。
subTestコンテキストマネージャーを使うことで、それぞれのテストケースを独立した単位として扱えます。
実行結果
F
======================================================================
FAIL: test_subtest (__main__.TestSubtest) (a=3, b=3, expected=6)
----------------------------------------------------------------------
Traceback (most recent call last):
File "test_subtest.py", line 12, in test_subtest
self.assertEqual(expected, actual)
AssertionError: 6 != 5
----------------------------------------------------------------------
Ran 1 test in 0.001s
FAILED (failures=1)
実行結果を見ると、3つのサブテストのうち1つが失敗していることがわかります。失
敗したサブテストのパラメータ(a=3, b=3, expected=6)も表示されているので、どのケースで失敗したのかが一目瞭然ですね。
サブテストを使えば、テストケースを整理してわかりやすくできます。
関連するテストをグループ化することで、テストの保守性も向上しますよ。
●assertEqualを使ったテストの実行
さて、assertEqualを使ってテストコードを書けるようになったところで、実際にテストを実行する方法を確認しておきましょう。
Pythonのunittestモジュールには、テストの実行を制御するための便利な機能が用意されています。
○unittestの実行方法
通常、unittestを使ったテストは、次のように実行します。
import unittest
class TestExample(unittest.TestCase):
def test_equal(self):
self.assertEqual(1 + 1, 2)
if __name__ == '__main__':
unittest.main()
このコードでは、TestExampleクラスにtest_equal関数を定義し、その中でassertEqualを使ってテストを行っています。
そして、if __name__ == '__main__'
のブロックで、unittest.main()を呼び出してテストを実行しているんです。
実行結果
.
----------------------------------------------------------------------
Ran 1 test in 0.001s
OK
テストがパスすると、このようにテスト結果が表示されます。
ドット(.)はテストが成功したことを示し、OKはすべてのテストがパスしたことを意味しています。
ただ、コマンドラインから直接unittestを実行することもできますよ。
$ python -m unittest test_example.py
このように、pythonコマンドに-mオプションを付けてunittestモジュールを指定し、テストファイル名を渡せば、テストを実行できます。
テストファイルが複数ある場合は、スペース区切りで指定しましょう。
○便利なオプション引数
unittestには、テストの実行をカスタマイズするための便利なオプション引数があります。
よく使われるオプションを見てみましょう。
verbosityオプション
$ python -m unittest -v test_example.py
-vオプションを付けると、テスト結果の出力が詳細になります。
テスト関数の名前や実行時間などの情報が表示されるので、テストの詳細を確認したい時に便利ですね。
failfastオプション
$ python -m unittest -f test_example.py
-fオプションを付けると、最初のテスト失敗時点でテストの実行が中断されます。
テストが失敗した際に素早くデバッグできるので、テスト駆動開発(TDD)のサイクルを回すのに役立ちますよ。
catchオプション
$ python -m unittest -c test_example.py
-cオプションを付けると、テスト実行中に発生したKeyboardInterrupt例外(Ctrl+Cによる中断)をキャッチして、テストを正常終了します。
テストの途中で中断したい場合に使えます。
まとめ
この記事では、PythonのunittestモジュールにおけるassertEqual関数の使い方について、実践的なテクニックを10個のサンプルコードとともに紹介してきました。
初心者の方も、ここで紹介したテクニックを参考にしながら、少しずつテストコードに挑戦してみてください。
最初は難しく感じるかもしれませんが、assertEqualの使い方が身についていけば、自然とテストを書くことが楽しくなってくるはずです。
assertEqualを自在に操れるようになって、バグのない高品質なコードを書けるエンジニアを目指しましょう。
これからのプログラミングライフが実り多きものになりますように!