本日は、こちらでLTをしました。
【20代限定・参加費無料】若手サーバエンジニア・サーバ業界関係者交流会2019 in Tokyo
登壇資料
それにあたり、今まで雰囲気で理解したmockについてすこーし知見を深めることが出来たので、忘れないうちに書き留めておきます。
以下、pythonの公式ドキュメントから適宜引用します。
[unittest — Unit testing framework — Python 3.7.4 documentation]
(https://docs.python.org/3/library/unittest.html)
テストコードの書き方
・下記のようにテストコードのなかでunittest.TestCaseを継承する
import unittest
from <filename> import <Classname>
class Test<Classname>(unittest.TestCase):
pass
・テストを実行するメソッドの名前はtest_xxx
とする。
・テストメソッドが実行されるたびに、はじめにsetUpメソッド、最後にtearDownメソッド
が呼ばれる。
import unittest
from <filename> import <Classname>
class Test<Classname>(unittest.TestCase):
def setUp(self):
self.object = Classname()
def tearDown(self):
self.objcect = None
self.result = None
def test_nomock(self):
self.result = self.object.main()
self.assertEqual(self.result, True)
※わざわざsetUpメソッドとtearDownメソッドを呼ばなくてもいいのですが、ここではインスタンスの生成と初期化をしていることを明示的に行いたかったため、書きました。
The simplest TestCase subclass will simply implement a test method (i.e. a method whose name starts with test) in order to perform specific testing code:
Such a working environment for the testing code is called a test fixture. A new TestCase instance is created as a unique test fixture used to execute each individual test method. Thus setUp(), tearDown(), and init() will be called once per test.
テストの成否は基本的なassertEqual(X, expectedX)
などとしています。
Method Checks that assertEqual(a, b) a == b assertNotEqual(a, b) a != b
テストコードの置き場所に応じたテスト実行コマンド
generaluser@localhost ~$ ls !(*.pyc) | grep sample.py
sample.py
test_sample.py
generaluser@localhost ~ $ python -m unittest test_sample
.....
----------------------------------------------------------------------
Ran 5 tests in 0.001s
OK
generaluser@localhost ~ $ mv test_sample.py tests/
generaluser@localhost ~ $ tree -L 3 -C | grep sample.py
├── sample.py
├── sample.pyc
│ ├── test_sample.py
│ ├── test_sample.pyc
└── test_sample.pyc
generaluser@localhost ~ $ python -m unittest tests.test_sample
.....
----------------------------------------------------------------------
Ran 5 tests in 0.001s
OK
generaluser@localhost ~ $
テストの対象となるサンプルコード
class Sample:
def challenge(self, conts):
"""
When conts is equal to a even number,
return value is true.
Otherwise, the value is false.
"""
if conts % 2 == 0:
return True
else:
return False
def main(self):
current_conts = 1
while True:
if not self.challenge(current_conts):
current_conts = current_conts + 1
continue
else:
break
return current_conts
mockを使わないでテストする場合
import unittest
from sample import Sample
class TestSample(unittest.TestCase):
def setUp(self):
""" Set object """
self.obj = Sample()
def tearDown(self):
""" Initiallize the object """
self.obj = None
self.number = None
def test00_no_mock(self):
""" test without mock """
self.number = self.obj.main()
self.assertEqual(self.number, 2)
mockで差し替えたメソッドの返り値を決め打ちで設定する
#MagickMockインスタンスをimport >>> from unittest.mock import MagicMock #テストの対象クラスを生成 >>> from sample import Sample >>> obj = Sample() >>> obj.method = MagicMock(return_value=3) >>> obj.method(3, 4, 5, key='value') 3 >> thing.method.assert_called_with(3, 4, 5, key='value')
import unittest
from mock import patch, MagicMock
from sample import Sample
class TestSample(unittest.TestCase):
def setUp(self):
""" Set object """
self.obj = Sample()
def tearDown(self):
""" Initiallize the object """
self.obj = None
self.number = None
@patch('sample.Sample.challenge', return_value=2)
def test01_mock_ok_return_value(self, chal):
chal.return_value
self.number = self.obj.main()
self.assertEqual(self.number, 1)
self.assertEqual(chal.call_count, 1)
@patch('sample.Sample.challenge')
def test02_mock_ok_return_value(self, chal):
chal.return_value = 2
self.number = self.obj.main()
self.assertEqual(self.number, 1)
self.assertEqual(chal.call_count, 1)
Q. logを吐くメソッドなど、テスト実行時には行いたくないメソッドがある場合
はどうすれば?
そのメソッドにreturn_value="dummy"などとpatch をあてれば大丈夫です。
その際、ダミー値をあててもテストが滞りなく実行されるように、そのメソッドと他のメソッドとの関係性を確認する必要があります。
たとえば、ダミー値をあてたwrite_logメソッドの返り値csv_fileは別のメソッドであるopen_logメソッドで使われる場合、open_logメソッドにpatchを当てて引数_filenameに既存のcsvファイルを当てるといった対応が必要でしょう。
@patch('sample.Sample.write_log', return_value="dummy")
@patch('sample.Sample.open_log')
def test_demo(self, logger, open):
demo_file = 'working_dir/log/logfile'
def _open_log(_filename)
#_filename="dummy"
output = []
with open(demo_file, newline='') as csvfile:
datas = csv.reader(csvfile, delimiter=' ', quotechar='|')
for data in datas:
try:
output.append(data)
except:
raise Exception("_open_log_method_failed")
return output
open.side_effect = _open_log
mockで差し替えたメソッドが呼ばれた回数に応じて返り値を変更する
import unittest
from mock import patch, MagicMock
from sample import Sample
class TestSample(unittest.TestCase):
def setUp(self):
""" Set object """
self.obj = Sample()
def tearDown(self):
""" Initiallize the object """
self.obj = None
self.number = None
@patch('sample.Sample.challenge')
def test03_mock_ok(self, chal):
""" when _conts is less than 4, return value is false.
Otherwise the value is true. """
def _challenge(_conts):
if chal.call_count <= 3:
return False
elif chal.call_count == 4:
return True
else:
raise Exception("no_more_challenges")
chal.side_effect = _challenge
self.number = self.obj.main()
self.assertEqual(self.number, 4)
self.assertNotEqual(self.number, 2)
self.assertEqual(chal.call_count, 4)
mockで差し替えたメソッドの引数の値に応じて返り値を変更する
import unittest
from mock import patch, MagicMock
from sample import Sample
class TestSample(unittest.TestCase):
def setUp(self):
""" Set object """
self.obj = Sample()
def tearDown(self):
""" Initiallize the object """
self.obj = None
self.number = None
@patch('sample.Sample.challenge')
def test03_mock_ok(self, chal):
def _challenge(_conts):
""" When conts is equal to a even number, return value is true.
Otherwise, the value is false. """
if _conts % 2 == 0:
return True
else:
return False
chal.side_effect = _challenge
self.number = self.obj.main()
self.assertEqual(self.number, 2)
self.assertNotEqual(self.number, 4)
self.assertEqual(chal.call_count, 2)
@patch('sample.Sample.challenge')
def test11_mock_ng(self, chal):
""" When conts is equal to a even number except 2, return value is true.
Otherwise, the value is false. """
def _challenge(_conts):
if _conts % 2 == 0:
if chal.call_count == 2:
return False
else:
return True
else:
return False
chal.side_effect = _challenge
self.number = self.obj.main()
self.assertNotEqual(self.number, 2)
self.assertEqual(self.number, 4)
self.assertEqual(chal.call_count, 4)
サンプルコード
Githubでもあげているので、よろしければ!
https://github.com/gkzz/mock_sample/
class Sample:
def challenge(self, conts):
"""
When conts is equal to a even number,
return value is true.
Otherwise, the value is false.
"""
if conts % 2 == 0:
return True
else:
return False
def main(self):
current_conts = 1
while True:
if not self.challenge(current_conts):
current_conts = current_conts + 1
continue
else:
break
return current_conts
import unittest
from mock import patch, MagicMock
from sample import Sample
class TestSample(unittest.TestCase):
def setUp(self):
""" Set object """
self.obj = Sample()
def tearDown(self):
""" Initiallize the object """
self.obj = None
self.number = None
def test00_no_mock(self):
""" test without mock """
self.number = self.obj.main()
self.assertEqual(self.number, 2)
@patch('sample.Sample.challenge', return_value=2)
def test01_mock_ok_return_value(self, chal):
chal.return_value
self.number = self.obj.main()
self.assertEqual(self.number, 1)
self.assertEqual(chal.call_count, 1)
@patch('sample.Sample.challenge')
def test02_mock_ok_return_value(self, chal):
chal.return_value = 2
self.number = self.obj.main()
self.assertEqual(self.number, 1)
self.assertEqual(chal.call_count, 1)
@patch('sample.Sample.challenge')
def test03_mock_ok(self, chal):
def _challenge(_conts):
""" when _conts is less than 4, return value is false.
Otherwise the value is true. """
if chal.call_count <= 3:
return False
elif chal.call_count == 4:
return True
else:
raise Exception("no_more_challenges")
chal.side_effect = _challenge
self.number = self.obj.main()
self.assertEqual(self.number, 4)
self.assertNotEqual(self.number, 2)
self.assertEqual(chal.call_count, 4)
@patch('sample.Sample.challenge')
def test04_mock_ok(self, chal):
def _challenge(_conts):
""" When conts is equal to a even number, return value is true.
Otherwise, the value is false. """
if _conts % 2 == 0:
return True
else:
return False
chal.side_effect = _challenge
self.number = self.obj.main()
self.assertEqual(self.number, 2)
self.assertNotEqual(self.number, 4)
self.assertEqual(chal.call_count, 2)
@patch('sample.Sample.challenge')
def test11_mock_ng(self, chal):
""" When conts is equal to a even number except 2, return value is true.
Otherwise, the value is false. """
def _challenge(_conts):
if _conts % 2 == 0:
if chal.call_count == 2:
return False
else:
return True
else:
return False
chal.side_effect = _challenge
self.number = self.obj.main()
self.assertNotEqual(self.number, 2)
self.assertEqual(self.number, 4)
self.assertEqual(chal.call_count, 4)
P.S. Twitterもやってるのでフォローしていただけると泣いて喜びます!
@gkzvoice