ほとんどすべての分野において、製品は市場に出る前に徹底的にテストされ、その品質と意図したとおりに機能することを保証している。
医薬品、化粧品、自動車、携帯電話、ノートパソコンなどは、消費者に約束した一定の品質が保たれているかどうかを確認するために、すべてテストされています。私たちの日常生活におけるソフトウェアの影響力と到達度を考えると、ソフトウェアを使用する際に問題が発生しないように、ユーザーにリリースする前に徹底的にテストすることが重要なのです。
ソフトウェアをテストする方法には様々なものがありますが、この記事ではUnittestフレームワークを使ったPythonプログラムのテストに焦点を当てます。
ユニットテストと他のテスト形式
ソフトウェアのテストには様々な方法があり、機能テストと非機能テストに大別される。
- 非機能テスト。非機能テスト:ソフトウェアの信頼性、セキュリティ、可用性、スケーラビリティなどの非機能的な側面を検証し、確認することを目的とする。非機能テストの例としては、負荷テストやストレステストなどがある。
- 機能テスト。機能テスト:ソフトウェアを機能要件に照らしてテストし、必要な機能を提供できることを確認する。例えば、ショッピングプラットフォームが注文後にユーザーにメールを送信するかどうか、そのシナリオをシミュレートし、メールの有無を確認することでテストすることができる。
ユニットテストは、インテグレーションテストやリグレッションテストと並んで、機能テストに該当する。
ユニットテストとは、ソフトウェアを異なるコンポーネント(ユニット)に分解し、各ユニットを機能的にテストし、他のユニットやモジュールから分離するテスト手法のことである。
ここでいうユニットとは、1つの機能を実現するシステムの最小の部分で、テスト可能なものを指す。ユニットテストの目的は、システムの各コンポーネントが期待通りに動作することを確認することであり、それによってシステム全体が機能要件を満たし、提供することを確認することである。
単体テストは、一般的に統合テストの前に実施される。なぜなら、システムの各部分が互いにうまく機能することを検証するためには、まず各部分が期待通りに動作することを確認しなければならないからである。また、一般的には、開発プロセスにおいて、個々のコンポーネントを構築する開発者が実施する。
B
ユニットテストのメリット
ユニットテストは、開発プロセスの早い段階でバグや問題を修正し、最終的に開発スピードを上げるという点で有益である。
また、ユニットテスト中に特定されたバグを修正するコストは、統合テスト中や本番中に修正するのに比べて低く抑えることができます。
ユニットテストは、よく書かれ、文書化されたテストを通じて、システムの各部分が何を行うかを定義することで、プロジェクトの文書化としての役割も果たします。システムのリファクタリングや機能追加を行う場合、ユニットテストは既存の機能を壊すような変更を防ぐのに役立ちます。
Unittestフレームワークの動作
簡単な電卓アプリケーションを作成し、期待通りに動作することを確認するためのテストを書くことで、unittest
フレームワークを探求していきます。テストから始めて、テストに合格するように機能を実装することで、テスト駆動開発プロセスを使用します。
Pythonアプリケーションを仮想環境で開発するのは良い習慣ですが、この例では、unittest
がPythonの配布物に同梱されており、電卓を構築するために他の外部パッケージが必要ないため、必須ではありません。
私たちの電卓は、2つの整数間の単純な足し算、引き算、掛け算、割り算を行う予定です。これらの要件は、unittest
フレームワークを用いた機能テストの指針となります。
私たちは電卓がサポートする 4 つの演算を別々にテストし、それぞれのテストを別々のテストスイートに書きます。テストスイートは1つのファイルに、電卓は別のファイルに格納されます。
計算機は SimpleCalculator
クラスで、期待される4つの操作を処理するための関数があります。まず、足し算のテストを test_simple_calculator.py
に書いてみましょう。
import unittest
from simple_calculator import SimpleCalculator
class AdditionTestSuite(unittest.TestCase):
def setUp(self):
""" Executed before every test case """
self.calculator = SimpleCalculator()
def tearDown(self):
""" Executed after every test case """
print("
tearDown executing after the test case. Result:")
def test_addition_two_integers(self):
result = self.calculator.sum(5, 6)
self.assertEqual(result, 11)
def test_addition_integer_string(self):
result = self.calculator.sum(5, "6")
self.assertEqual(result, "ERROR")
def test_addition_negative_integers(self):
result = self.calculator.sum(-5, -6)
self.assertEqual(result, -11)
self.assertNotEqual(result, 11)
# Execute all the tests when the file is executed
if __name__ == "__main__":
unittest.main()
まず、unittest
モジュールをインポートして、加算操作のテストスイート(AdditionTestSuite
)を作成します。
setUp()メソッドは各テストケースの前に呼び出され、計算を行うための
SimpleCalculator` オブジェクトを生成します。
tearDown()` メソッドは各テストケースの後に実行されますが、今のところあまり使い道がないので、各テストの結果を出力するためだけに使用することにします。
関数 test_addition_two_integers()
, test_addition_integer_string()
と test_addition_negative_integers()
はテストケースです。この電卓は、2つの正または負の整数を加算してその和を返すことが期待されています。整数と文字列を提示された場合、電卓はエラーを返すことになっています。
assertEqual()と
assertNotEqual()は、電卓の出力を検証するために使用する関数です。この例では、
5と
6の和が
11` であることを想定しているので、電卓が返す値と比較します。
もし2つの値が等しければ、テストはパスです。その他、 unittest
が提供するアサーション関数には以下のものがあります。
-
assertTrue(a)
: 与えられた式がtrue
であるかどうかをチェックします。 -
assertGreater(a, b)
: aが
b` よりも大きいかどうかをチェックする。 -
assertNotIn(a, b)
: aが
b` の中にあるかどうかをチェックする。 -
assertLessEqual(a, b)
: aが
b` より小さいか等しいかどうかをチェックします。 - etc….
これらのアサーションの一覧は、このチートシートで見ることができます。
テストファイルを実行すると、このように出力されます。
$ python3 test_simple_calulator.py
tearDown executing after the test case. Result:
E
tearDown executing after the test case. Result:
E
tearDown executing after the test case. Result:
E
======================================================================
ERROR: test_addition_integer_string (__main__.AdditionTestSuite)
----------------------------------------------------------------------
Traceback (most recent call last):
File "test_simple_calulator.py", line 22, in test_addition_integer_string
result = self.calculator.sum(5, "6")
AttributeError: 'SimpleCalculator' object has no attribute 'sum'
======================================================================
ERROR: test_addition_negative_integers (__main__.AdditionTestSuite)
----------------------------------------------------------------------
Traceback (most recent call last):
File "test_simple_calulator.py", line 26, in test_addition_negative_integers
result = self.calculator.sum(-5, -6)
AttributeError: 'SimpleCalculator' object has no attribute 'sum'
======================================================================
ERROR: test_addition_two_integers (__main__.AdditionTestSuite)
----------------------------------------------------------------------
Traceback (most recent call last):
File "test_simple_calulator.py", line 18, in test_addition_two_integers
result = self.calculator.sum(5, 6)
AttributeError: 'SimpleCalculator' object has no attribute 'sum'
----------------------------------------------------------------------
Ran 3 tests in 0.001s
FAILED (errors=3)
出力の一番上に、指定したメッセージが表示され、 tearDown()
関数が実行されたことがわかります。続いて E
という文字と、テストの実行によって発生したエラーメッセージが表示されます。
テストには3つの結果があり、合格、失敗、エラーとなります。unittest` フレームワークでは、3つのシナリオを以下のように表します。
- 完全停止 (
.
)。テストに合格したことを表します。 - 文字 ‘F’。テストが失敗したことを示します。
- 文字 ‘E’: テストの実行中にエラーが発生したことを示します。
私たちの場合、E
という文字が表示されていますが、これはテストの実行時にエラーが発生したことを意味します。電卓の 追加
機能がまだ実装されていないため、エラーが発生しているのです。
class SimpleCalculator:
def sum(self, a, b):
""" Function to add two integers """
return a + b
しかし、期待通りに動作することを確認するために、テストから tearDown()
関数を削除して、もう一度テストを実行してみましょう。
$ python3 test_simple_calulator.py
E..
======================================================================
ERROR: test_addition_integer_string (__main__.AdditionTestSuite)
----------------------------------------------------------------------
Traceback (most recent call last):
File "test_simple_calulator.py", line 22, in test_addition_integer_string
result = self.calculator.sum(5, "6")
File "/Users/robley/Desktop/code/python/unittest_demo/src/simple_calculator.py", line 7, in sum
return a + b
TypeError: unsupported operand type(s) for +: 'int' and 'str'
----------------------------------------------------------------------
Ran 3 tests in 0.002s
FAILED (errors=1)
エラーは3回から1回に減りました。最初の行 E..
のレポートサマリーは、1つのテストがエラーとなり実行を完了できなかったこと、そして残りの2つがパスしたことを示しています。最初のテストをパスさせるためには、sum関数を以下のようにリファクタリングする必要があります。
def sum(self, a, b):
if isinstance(a, int) and isinstance(b, int):
return a + b
もう一回テストを実行すると
コード偽
今度は、sum 関数は完了まで実行されましたが、テストは失敗しました。これは、入力のひとつが整数でないときに値を返さなかったからです。アサーションは None
と ERROR
を比較し、両者は等しくないのでテストは失敗します。テストに合格するには、sum()
関数でエラーを返さなければなりません。
$ python3 test_simple_calulator.py
F..
======================================================================
FAIL: test_addition_integer_string (__main__.AdditionTestSuite)
----------------------------------------------------------------------
Traceback (most recent call last):
File "test_simple_calulator.py", line 23, in test_addition_integer_string
self.assertEqual(result, "ERROR")
AssertionError: None != 'ERROR'
----------------------------------------------------------------------
Ran 3 tests in 0.001s
FAILED (failures=1)
そしてテストを実行すると
def sum(self, a, b):
if isinstance(a, int) and isinstance(b, int):
return a + b
else:
return "ERROR"
すべてのテストが合格し、3つのフルストップが表示されました。これは足し算の機能に関する3つのテストがすべて合格したことを示しています。引き算、掛け算、割り算のテストスイートも、同様の方法で実装されています。
また、例外が発生した場合のテストも可能です。例えば、ある数字を 0 で割ったとき、 ZeroDivisionError
という例外が発生します。私たちの DivisionTestSuite
では、例外が発生したかどうかを確認することができる。
$ python3 test_simple_calulator.py
...
----------------------------------------------------------------------
Ran 3 tests in 0.000s
OK
テストサイト `test_divi
結論
この記事では、unittest
フレームワークについて調べ、Pythonプログラムを開発する際に使用される状況を確認しました。PyUnitとしても知られるunittest
フレームワークは、他のテストフレームワークとは異なり、Pythonのディストリビューションにデフォルトで付属しています。TDD-mannerでは、簡単な電卓のテストを書き、テストを実行し、テストに合格するように機能を実装しました。
unittest` フレームワークはテストケースを作成し、グループ化する機能を提供し、期待される出力に対して計算機の出力をチェックし、期待通りに動作していることを検証しました。
完全な計算機とテストスイートはGitHubのこのgistで見ることができます。