1
4

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 3 years have passed since last update.

Pythonのunittestを最低限扱えるようになる

Posted at

簡単なテストを書いてみる

utils.py
def concat(str1: str, str2: str) -> str:
    if not isinstance(str1, str) and isinstance(str2, str):
        raise TypeError

    return str1 + str2
utils_test.py
from unittest import TestCase, main

from utils import concat

class UtilsTestCase(TestCase):
    def test_good_for_concat(self):
        test_cases = [
            (('a', 'b'), 'ab'),
            (('test', 'case'), 'testcase'),
        ]
        for value, expected in test_cases:
            with self.subTest(value):
                self.assertEqual(expected, concat(value[0], value[1]))

    def test_bad_for_concat(self):
        test_cases = [
            (('a', 2), TypeError),
            ((1, 'b'), TypeError),
        ]
        for value, exception in test_cases:
            with self.subTest(value):
                with self.assertRaises(exception):
                    concat(value[0], value[1])

if __name__ == '__main__':
    main()

実行結果

  • --verbose(-v)で詳細な出力を出せる
$ python3 -m unittest utils_test.UtilsTestCase 
..
----------------------------------------------------------------------
Ran 2 tests in 0.000s

OK
$ python3 -m unittest utils_test.UtilsTestCase --verbose
test_bad_for_concat (utils_test.UtilsTestCase) ... ok
test_good_for_concat (utils_test.UtilsTestCase) ... ok

----------------------------------------------------------------------
Ran 2 tests in 0.001s

OK

., F, Eって何?

  • テストには3つの結果がある
    • ok(.): テストに合格
    • FAIL(F): テストに合格せず、AssertionError例外が発生
    • ERROR(E): テストでAssertionError以外の例外が発生
ok_fail_error.py
from unittest import TestCase, main

class OkFailErrorTestCase(TestCase):
    def test_ok(self):
        self.assertEqual(1, 1)

    def test_fail(self):
        self.assertEqual(1, 2)

    def test_error(self):
        import requests
        self.assertEqual(1, 1)

if __name__ == '__main__':
    main()

実行結果

$ python3 -m unittest ok_fail_error.OkFailErrorTestCase
EF.
======================================================================
ERROR: test_error (__main__.OkFailErrorTestCase)
----------------------------------------------------------------------
Traceback (most recent call last):
  File "ok_fail_error.py", line 5, in test_error
    import requests
ModuleNotFoundError: No module named 'requests'

======================================================================
FAIL: test_fail (__main__.OkFailErrorTestCase)
----------------------------------------------------------------------
Traceback (most recent call last):
  File "ok_fail_error.py", line 12, in test_fail
    self.assertEqual(1, 2)
AssertionError: 1 != 2

----------------------------------------------------------------------
Ran 3 tests in 0.001s

FAILED (failures=1, errors=1)

setUp, tearDownを使ってみる

  • setUp()
    • 各テストメソッドのに実行する命令を実装可能
  • tearDown()
    • 各テストメソッドのに実行する命令を実装可能
  • setUpModule()
    • 一つのクラスやモジュールにつきテスト開始時に一度だけ実行する命令を実装可能
  • tearDownModule()
    • 一つのクラスやモジュールにつきテスト終了時に一度だけ実行する命令を実装可能
  • setUpClass()
    • 一つのクラスやモジュールにつきテスト開始時に一度だけ実行する命令を実装可能
  • tearDownClass()
    • 一つのクラスやモジュールにつきテスト終了時に一度だけ実行する命令を実装可能
integration_test.py
from unittest import TestCase, main

def setUpModule():
    print('* Module setup')

def tearDownModule():
    print('* Module clean-up')

class IntegrationTest(TestCase):
    def setUp(self):
        print('** Test setup')

    def tearDown(self):
        print('** Test clean-up')

    def test_1(self):
        print('** Test 1')

    def test_2(self):
        print('** Test 2')

if __name__ == '__main__':
    main()

実行結果

$ python3 -m unittest integration_test.IntegrationTest --verbose
* Module setup
test_1 (integration_test.IntegrationTest) ... ** Test setup
** Test 1
** Test clean-up
ok
test_2 (integration_test.IntegrationTest) ... ** Test setup
** Test 2
** Test clean-up
ok
* Module clean-up

----------------------------------------------------------------------
Ran 2 tests in 0.000s

OK

integration2_test.py
from unittest import TestCase, main

class Integration2Test(TestCase):
    @classmethod
    def setUpClass(cls):
        print('* Class setup')

    @classmethod
    def tearDownClass(cls):
        print('* Class clean-up')

    def setUp(self):
        print('** Test setup')

    def tearDown(self):
        print('** Test clean-up')

    def test_1(self):
        print('** Test 1')

    def test_2(self):
        print('** Test 2')

if __name__ == '__main__':
    main()

実行結果

$ python3 -m unittest integration2_test.Integration2Test --verbose
* Class setup
test_1 (integration2_test.Integration2Test) ... ** Test setup
** Test 1
** Test clean-up
ok
test_2 (integration2_test.Integration2Test) ... ** Test setup
** Test 2
** Test clean-up
ok
* Class clean-up

----------------------------------------------------------------------
Ran 2 tests in 0.000s

OK

mock, patchを使ってみる

  • mock
    • Pythonのソフトウェアテストのためのライブラリ
    • テストにおいて、システムの一部を Mock オブジェクトで置き換えることで、それらがどのように使われるかをアサートすることが可能
  • patch
    • 特定のモジュール内のクラスを Mock オブジェクトで一時的に置換可能
>>> from mock import Mock
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
ModuleNotFoundError: No module named 'mock'
>>> from unittest.mock import Mock
>>> m = Mock()
>>> m
<Mock id='4449643664'>
>>> m()
<Mock name='mock()' id='4449643728'>
>>> m.return_value = 25
>>> m
<Mock id='4449643664'>
>>> m()
25
>>> m.a
<Mock name='mock.a' id='4449643728'>
>>> m.a = 10
>>> m.a
10
>>> m.a.return_value = 20
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
AttributeError: 'int' object has no attribute 'return_value'
>>> 

my_class.py
import requests

class MyClass:
    def fetch_json(self, url: str) -> dict:
        response = requests.get(url)
        return response.json()
my_class_test.py
from unittest import TestCase, main
from unittest.mock import MagicMock, call, patch

from my_class import MyClass

class MyClassTestCase(TestCase):
    def mocked_requests_get(*args, **kwargs):
        class MockResponse:
            def __init__(self, json_data, status_code):
                self.json_data = json_data
                self.status_code = status_code

            def json(self):
                return self.json_data

        if args[0] == 'http://example.com/test.json':
            return MockResponse({'key1': 'value1'}, 200)
        elif args[0] == 'http://example.com/another_test.json':
            return MockResponse({'key2': 'value2'}, 200)

        return MockResponse(None, 404)

    @patch('requests.get', side_effect=mocked_requests_get)
    def test_fetch_json(self, mock_get: MagicMock):
        my_class = MyClass()

        json_data = my_class.fetch_json('http://example.com/test.json')
        self.assertEqual(json_data, {'key1': 'value1'})
        json_data = my_class.fetch_json('http://example.com/another_test.json')
        self.assertEqual(json_data, {'key2': 'value2'})
        json_data = my_class.fetch_json('http://no_example.com/test.json')
        self.assertIsNone(json_data)

        self.assertIn(
            call('http://example.com/test.json'), mock_get.call_args_list
        )
        self.assertIn(
            call('http://example.com/another_test.json'), mock_get.call_args_list
        )

        self.assertEqual(len(mock_get.call_args_list), 3)

if __name__ == '__main__':
    main()

実行結果

$ python3 -m unittest my_class_test.MyClassTestCase --verbose
test_fetch_json (my_class_test.MyClassTestCase) ... ok

----------------------------------------------------------------------
Ran 1 test in 0.002s

OK

ファイル名、クラス名、メソッド名

  • メソッド名
    • testで始まる名前にしないといけない
  • ファイル名
    • なんでもよい
    • --pattern(-p)のデフォルトがtest*.pyなのでtest_*.pyにするのが無難か
  • クラス名
    • なんでもよい
    • メソッド名、ファイル名に合わせてTest*にするのが無難か

参考

1
4
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
4

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?