はじめに
pythonで標準モジュールとして使えるunittestモジュールの基本的な使い方を順を追って記していきます。
テスト環境
テストを実施する際の環境を以下のように準備します。
- 動作確認 python3.7
- テスト対象のスクリプト --- honban_file.py
- テストを実行する為のスクリプト --- unit_test.py
テスト実施の流れ
以下の流れでunittestモジュールの使い方を見ていきます。
- テスト対象のスクリプト の内容を確認
- テストを実行するスクリプト の内容を確認
- テストの実行結果 を確認
テストコードの作成・実行1
最初にシンプルな内容のテストを実施します。
テスト対象のスクリプト
テストを行う対象のファイル(honban_file.py)が以下のような内容だとします。
honban_file.py
# coding: utf-8
class Functions :
def plus(self, a, b):
return a + b
- honban_file.pyは、Functionsクラスに足し算を行うplusメソッドが記載されています。
- plusメソッドは、引数を2つ受け取り、足し算を行います。
- このplusメソッドが正しく動作しているかを確認するテストを行います。
テストを実行するスクリプト
次に、ユニットテストのコードを以下のように作成します。
unit_test.py
import unittest
import honban_file
honban = honban_file.Functions()
class TestKeisanNormal(unittest.TestCase):
def test_tashizan_normal(self):
value1 = 2
value2 = 6
expected = 8
result = honban.plus(value1, value2)
self.assertEqual(expected, result)
if __name__ == "__main__":
# コマンドラインで実行する場合は以下。
# unittest.main()
# IPython あるいは Jupyter で実行する場合は以下。
unittest.main(argv=['first-arg-is-ignored'], exit=False)
- import honban_file でテスト対象のスクリプトを呼び出します。
- test_tashizan_normalメソッドで実際のテスト内容を書きます。
- assertEqualで、テスト対象の結果と期待する結果が一致しているかをチェックしています。
- unittestをJupyterなどで試す場合は、unittest.main の箇所に少々工夫が必要になります。
( 詳細: https://medium.com/@vladbezden/using-python-unittest-in-ipython-or-jupyter-732448724e31 )
テストの実行結果
以下はテストを実行し、テストが期待通りに終了した例です。
実行コマンド
# python unit_test.py
実行結果
.
----------------------------------------------------------------------
Ran 1 test in 0.005s
OK
テストに失敗すると以下のような結果となります。
失敗したテストが、Fと表示されます。
F
======================================================================
FAIL: test_tashizan_normal (__main__.TestKeisanNormal)
----------------------------------------------------------------------
Traceback (most recent call last):
File "<ipython-input-1-f34dc9daa684>", line 13, in test_tashizan_normal
self.assertEqual(expected, result)
AssertionError: 9 != 8
----------------------------------------------------------------------
Ran 1 test in 0.004s
FAILED (failures=1)
なお、テスト・コード自体が間違っていると以下のようにEと表示されます。
E
======================================================================
ERROR: test_tashizan_normal (__main__.TestKeisanNormal)
----------------------------------------------------------------------
Traceback (most recent call last):
File "<ipython-input-1-78bd8c44601e>", line 12, in test_tashizan_normal
result = honban.plus(value1)
TypeError: plus() missing 1 required positional argument: 'b'
----------------------------------------------------------------------
Ran 1 test in 0.004s
FAILED (errors=1)
テストコードの作成・実行2
先ほどは、テスト対象のメソッドが引数を2つ受け取る形でした。今回は、クラス変数が存在する場合を見てみます。
テスト対象のスクリプト
今度は、本番ファイルを以下のように書き換えます。
honban_file.py
# coding: utf-8
class Functions() :
def __init__(self, a, b) :
self.a = a
self.b = b
def minus(self,):
a = self.a
b = self.b
return a - b
テストを実行するスクリプト
class TestKeisan2(unittest.TestCase):
def test_hikizan(self):
honban = honban_file.Functions(3, 2)
expected = 1
result = honban.minus()
self.assertEqual(expected, result)
if __name__ == "__main__":
# コマンドラインで実行する場合は以下。
# unittest.main()
# IPython あるいは Jupyter で実行する場合は以下。
unittest.main(argv=['first-arg-is-ignored'], exit=False)
実行結果
クラス変数に3と2を与えて、計算結果は想定通りの1となりました。テスト結果の結果は成功となりました。
.
----------------------------------------------------------------------
Ran 1 test in 0.007s
OK
テストコードの作成・実行3
今度は、本番ファイルのメソッドがリターンする値を強制的に変更してテストしてみます。
テスト対象のスクリプト
テスト対象のスクリプト「honban_file.py」の内容は、minusメソッドが0を返すことになっています。
honban_file.py
# coding: utf-8
class Functions :
def minus(self,):
a = 3
b = 3
return a - b
テスト・スクリプト
本番スクリプトのminusメソッドがリターンする数字を強制的に変更していきます。上記のminusメソッドでは、本来であれば、0がリターンされるはずですが、1がリターンされるようにテストスクリプトで上書きします。
honban_file.py
import unittest
import honban_file
honban = honban_file.Functions()
class TestKeisan3(unittest.TestCase):
def test_hikizan(self):
def mock_func(*args):
return 1
honban.minus = mock_func
result = honban.minus()
expected = 1
self.assertEqual(expected, result)
if __name__ == "__main__":
# コマンドラインで実行する場合は以下。
# unittest.main()
# IPython あるいは Jupyter で実行する場合は以下。
unittest.main(argv=['first-arg-is-ignored'], exit=False)
- テストを行うメソッド「def test_hikizan(self)」内に、更に関数「def mock_func(*args):」を作ります。
- 本来は、0をリターンするminus()メソッドに対して、mock_func(*args)を代入します。
- 結果として、本来は0をリターンするminus()メソッドが1をリターンするようになります。
テストの実行結果
.
----------------------------------------------------------------------
Ran 1 test in 0.008s
OK
テストコードの作成・実行4 組み込み関数
今回は、組み込み関数の出力内容を変更するテスト方法を実行します。
テスト対象のスクリプト
honban_file.py
# coding: utf-8
class Functions :
def output_str(self,):
number = 1
string = str(number)
return string
- 数字の型を、組み込み関数のstr()を使って文字の型に変更しています。
- 通常であれば、output_strメソッドは、文字型の1を返すはずですが、テストを実行するスクリプトで組み込み関数のstr()が、数字の2をリターンするように変更してみます。
テストを実行するスクリプト
honban_file.py
# coding: utf-8
import unittest
import honban_file
honban = honban_file.Functions()
class TestCasedes(unittest.TestCase):
def test_str_mock(self):
def new_str(*args):
return 2
__builtins__.str = new_str
result = honban.output_str()
expected = 2
print(result) # 2 が出力される。
self.assertEqual(expected, result)
if __name__ == "__main__":
# コマンドラインで実行する場合は以下。
unittest.main()
- テストを行うメソッド「def test_str_mock(self):」の中に新たな関数「def new_str(self):」を作成しています。
- 組み込み関数str()の内容を、new_str()関数で上書きしています。
- 本コードは、Jupyterでは動きませんでしたので、試してみる場合は、プロンプトなどから実行してみてください。
テストの実行結果
以下のように成功します。想定通りに2も出力されています。
2
.
----------------------------------------------------------------------
Ran 1 test in 0.002s
OK
補足 1
上記で組み込み関数を上書きしていますが、組み込み関数を上書きする場合は、組み込み関数の中身を下記のように元に戻してあげるとよいでしょう。
class TestCasedes(unittest.TestCase):
def test_str_mock(self):
def new_str(*args):
return 2
original_str = __builtins__.str # 元の組み込み関数str()の内容を保存します。
__builtins__.str = new_str
result = honban.output_str()
__builtins__.str = original_str # 元の組み込み関数str()の内容に戻します。
expected = 2
print(result)
self.assertEqual(expected, result)
補足 2
今回は、組み込み関数str()の返り値を変更するために、「builtins.str」という形で呼び出しました。
しかし、本番ファイル(honban_file.py)のstr()関数を変更する場合は、以下のようにhonban_file.str()という形でも呼び出せます。
import unittest
import honban_file
honban = honban_file.Functions()
class TestCasedes(unittest.TestCase):
def test_str_mock(self):
def new_str(*args):
return 2
honban_file.str = new_str # ここでのstr()関数の呼び出し方が違います。
result = honban.output_str()
expected = 2
print(result) # 2 が出力される。
self.assertEqual(expected, result)
if __name__ == "__main__":
# コマンドラインで実行する場合は以下。
unittest.main()
テストコードの作成・実行5 ファイル読み込み
前回は、組み込み関数str()の返り値を変更してみました。今回は、実践的なテストを想定してファイルの内容を読み取るopen()関数の返り値を変更していきます。
テスト対象の本番ファイル内で、ファイルの読み取りを行っている場合に、その読み取るファイルの内容を変更してみます。
テスト対象のスクリプト
honban_file.py
# coding: utf-8
class Functions :
def read_file(self,):
path = "./honban.txt"
with open(path) as f:
line = f.read()
return line
honban.txt
honban-kakikomi
- honban_file内で、テキストファイル(honban.txt)を組み込み関数のopenで開いています。
- テキストファイル(honban.txt)の内容を読み込むと、「honban-kakikomi」と書かれています。
- 最終的に、「honban-kakikomi」をread_file()のメソッドがリターンします。
テストを実行するスクリプト
- honban_file内で、テキストファイル(honban.txt)を組み込み関数のopenで開いた際に、どのような内容が読み込まれるかをテストスクリプトで指定します。
- 比較のために、普通にテキストファイル(honban.txt)を読み込んで中身をテストする方法と、読み込む中身を変更する方法の両方を記載していきます。
honban_file.py
import unittest
from unittest.mock import mock_open
import honban_file
honban = honban_file.Functions()
class Test5(unittest.TestCase):
def test_read_file_honban(self):
result = honban.read_file()
expected = "honban-kakikomi"
print(result)
self.assertEqual(expected, result)
def test_read_file_mock(self):
changed_open = mock_open(read_data='test-kakikomi')
__builtins__.open = changed_open
result = honban.read_file()
expected = "test-kakikomi"
print(result)
self.assertEqual(expected, result)
if __name__ == "__main__":
# コマンドラインで実行する場合は以下。
# unittest.main()
# IPython あるいは Jupyter で実行する場合は以下。
unittest.main(argv=['first-arg-is-ignored'], exit=False)
- 「def test_read_file_honban(self):」が普通にテキストファイル(honban.txt)を読み込んでテストしています。「def test_read_file_mock(self):」で、組み込み関数(builtins.open) の内容を変更しています。
- 組み込み関数(builtins.open) の内容を、「mock_open」関数で変更しています。(mock_open関数は、「from unittest.mock import mock_open」でimportします。)
- 「mock_open」関数実行時の「read_data='test-kakikomi'」で、open関数を実行した際にリターンされる内容を指定しています。
テストの実行結果
..
honban-desu
test-desu
----------------------------------------------------------------------
Ran 2 tests in 0.018s
OK
補足
デコレータ(@patch)を使って以下のように書くこともできます。
honban_file.py
import unittest
from unittest.mock import mock_open
from unittest.mock import patch
import honban_file
honban = honban_file.Functions()
class Test5(unittest.TestCase):
def test_read_file_honban(self):
result = honban.read_file()
expected = "honban-kakikomi"
self.assertEqual(expected, result)
@patch('honban_file.open')
def test_read_file_mock(self, MockOpen):
mock_open(mock=MockOpen, read_data='test-kakikomi')
result = honban.read_file()
expected = "test-kakikomi"
self.assertEqual(expected, result)
if __name__ == "__main__":
# コマンドラインで実行する場合は以下。
# unittest.main()
# IPython あるいは Jupyter で実行する場合は以下。
unittest.main(argv=['first-arg-is-ignored'], exit=False)
補足
今回の例では、テストとして期待値と実際の値が同じかどうか(assertEqual(expected, result))をチェックしました。
その他のメソッドとして、主なものを下記に列記しておきます。
メソッド | 確認内容 | 追加されたversion |
---|---|---|
assertEqual(a, b) | a == b | |
assertNotEqual(a, b) | a != b | |
assertTrue(x) | bool(x) is True | |
assertFalse(x) | bool(x) is False | |
assertIs(a, b) | a is b | 3.1 |
assertIsNot(a, b) | a is not b | 3.1 |
assertIsNone(x) | x is None | 3.1 |
assertIsNotNone(x) | x is not None | 3.1 |
assertIn(a, b) | a in b | 3.1 |
assertNotIn(a, b) | a not in b | 3.1 |
assertIsInstance(a, b) | isinstance(a, b) | 3.2 |
assertNotIsInstance(a, b) | not isinstance(a, b) | 3.2 |
assertGreater(a, b) | a > b | 3.1 |
assertGreaterEqual(a, b) | a >= b | 3.1 |
assertLess(a, b) | a < b | 3.1 |
assertLessEqual(a, b) | a <= b | 3.1 |
https://docs.python.org/3/library/unittest.html
参考文献
https://docs.python.org/ja/3.7/library/unittest.mock.html
https://qiita.com/amedama/items/5aff37c75c28b58386d2
https://qiita.com/FGtatsuro/items/eea263bc15ebfb9767cd