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

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

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

    return str1 + str2
```

```python: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以外の例外が発生

```python: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()`
    - 一つのクラスやモジュールにつき**テスト終了時**に一度だけ実行する命令を実装可能

```python: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
```

---

```python: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 オブジェクトで一時的に置換可能

```python
>>> 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'
>>> 
```

---

```python:my_class.py
import requests

class MyClass:
    def fetch_json(self, url: str) -> dict:
        response = requests.get(url)
        return response.json()
```



```python: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*`にするのが無難か

## 参考

https://docs.python.org/ja/3/library/unittest.html

https://pymotw.com/2/unittest/

https://stackoverflow.com/questions/15753390/how-can-i-mock-requests-and-the-response