Pythonでユニットテストする

# はじめに

標準で入ってるユニットテストのフレームワークである unittest を使ってみる。

Python2 では必須だった `__init__.py` は Python3.3 以降であれば不要となった。

# 環境

Python3.6

# 方法1: もっともシンプルな構造

ファイル名を指定したテスト実行はできない手段です。

## ディレクトリ、ファイル構造

```console
project
├── a.py
└── tests
    └── test_add.py
```

## テスト実行方法

### テスト範囲を探して、すべてをテストする

テストが実行される。実行結果は、失敗。

```console
$ python -m unittest discover tests
F
======================================================================
FAIL: test_add (test_add.TestAdd)
----------------------------------------------------------------------
Traceback (most recent call last):
  File "/home/kumar55/project/tests/test_add.py", line 10, in test_add
    self.assertEqual(expected, actual)
AssertionError: 5 != -1

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

FAILED (failures=1)
```

### テスト範囲をパッケージで指定する

テストが実行される。実行結果は、失敗。

```console
$ python -m unittest tests.test_add
F
======================================================================
FAIL: test_add (tests.test_add.TestAdd)
----------------------------------------------------------------------
Traceback (most recent call last):
  File "/home/kumar55/project/tests/test_add.py", line 10, in test_add
    self.assertEqual(expected, actual)
AssertionError: 5 != -1

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

FAILED (failures=1)
```

### テスト範囲をファイル名で指定する

モジュールのインポート上、テスト実行以前に失敗する。

```console
$ python tests/test_add.py
Traceback (most recent call last):
  File "tests/test_add.py", line 2, in <module>
    import a
ModuleNotFoundError: No module named 'a'
```

## コード

```bash
mkdir -pv ./project

cat <<'__PYTHON__' | tee ./project/a.py
#!/usr/bin/env python3
# coding: utf-8

def add(x, y):
    return x - y

if __name__ == '__main__':
    print(add(1,2))
__PYTHON__
chmod -v +x ./project/a.py

mkdir -pv ./project/tests

cat <<'__PYTHON__' | tee ./project/tests/test_add.py
import unittest
import a

class TestAdd(unittest.TestCase):
    def test_add(self):
        x = 2
        y = 3
        expected = 5
        actual = a.add(x, y)
        self.assertEqual(expected, actual)

if __name__ == "__main__":
    unittest.main()
__PYTHON__
```

# 方法2: ファイル名を指定したテスト実行ができる方法

方法1の対処として:

* a.pyのモジュールのパスを加える
   * ただし、PEP8に準拠し、import 前に sys.path への追加をしない実装とするため、pathmagic.pyにモジュールのパスを加える実装を書き、pathmagicを各テストコードでimportする。

なお、代償というわけではないが、「方法1」でパッケージ名を指定したテスト実行ができたが、これができなくなる。後述。

## ディレクトリ、ファイル構造

```console
project
├── a.py
└── tests
    ├── pathmagic.py ★追加
    └── test_add.py
```

## テスト実行方法

### テスト範囲を探して、すべてをテストする

テストが実行される。実行結果は、失敗。

```console
$ cd ./project
$ python -m unittest discover tests
F
======================================================================
FAIL: test_add (test_add.TestAdd)
----------------------------------------------------------------------
Traceback (most recent call last):
  File "/home/kumar55/project/tests/test_add.py", line 13, in test_add
    self.assertEqual(expected, actual)
AssertionError: 5 != -1

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

FAILED (failures=1)
```

### テスト範囲をパッケージで指定する

pathmagic.py は、tests に配置され、 tests はパスに含まれてないため、「方法1」で実行できたこの指定方法が失敗する。

```console
$ python -m unittest tests.test_add
E
======================================================================
ERROR: test_add (unittest.loader._FailedTest)
----------------------------------------------------------------------
ImportError: Failed to import test module: test_add
Traceback (most recent call last):
  File "/usr/lib64/python3.6/unittest/loader.py", line 153, in loadTestsFromName
    module = __import__(module_name)
  File "/home/kumar55/project/tests/test_add.py", line 3, in <module>
    import pathmagic
ModuleNotFoundError: No module named 'pathmagic'


----------------------------------------------------------------------
Ran 1 test in 0.001s

FAILED (errors=1)
```

ちなみに、この対処のためにさらに pathmagic.py に手を加えればいいじゃないか、と考えたくなるがむつかしい。

まず pathmagic.py に tests を加えることは対処にならない。そもそも pathmagic.py を見つけることができないという状況なので対処できない。

次に a.py か test_add.py のどちらかに手を加えることを考えるが a.py に手を加える手はテストのためだけにテスト対象に手を加えることになるので良い手段とは言いにくい。test_add.py に import を使わず、importlibを使って動的にロードすればできるはずだが、まだ試してない。ただ、できたとしても、 test_add.py の隣に test_xxx.py を置いたときに、同様のコードが増えるので悪手になりやすい。

### テスト範囲をファイル名で指定する

テストが実行される。実行結果は、失敗。

```console
$ python tests/test_add.py
F
======================================================================
FAIL: test_add (__main__.TestAdd)
----------------------------------------------------------------------
Traceback (most recent call last):
  File "tests/test_add.py", line 13, in test_add
    self.assertEqual(expected, actual)
AssertionError: 5 != -1

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

FAILED (failures=1)
```

## コード

```bash
mkdir -pv ./project

cat <<'__PYTHON__' | tee ./project/a.py >/dev/null
#!/usr/bin/env python3
# coding: utf-8


def add(x, y):
    return x - y


if __name__ == '__main__':
    print(add(1, 2))
__PYTHON__
chmod -v +x ./project/a.py

mkdir -pv ./project/tests

cat <<'__PYTHON__' | tee ./project/tests/pathmagic.py >/dev/null
from pathlib import Path
import sys


_tests_dir = Path(__file__).parent
_base_dir = Path.resolve(_tests_dir.parent)
_mod_dir = str(Path.resolve(_base_dir))
sys.path.append(_mod_dir)
__PYTHON__

cat <<'__PYTHON__' | tee ./project/tests/test_add.py >/dev/null
#!/usr/bin/env python3
import unittest
import pathmagic
import a


class TestAdd(unittest.TestCase):
    def test_add(self):
        x = 2
        y = 3
        expected = 5
        actual = a.add(x, y)
        self.assertEqual(expected, actual)


if __name__ == "__main__":
    unittest.main()
__PYTHON__
chmod -v +x ./project/tests/test_add.py
```

# 参考資料

Python2でユニットテストする
https://qiita.com/kumarstack55/items/f07a2c5c783ac64fbcf1

`__init__.py` がなくてもpackageをimportできる
https://qiita.com/ysk24ok/items/2711295d83218c699276#__init__py%E3%81%8C%E3%81%AA%E3%81%8F%E3%81%A6%E3%82%82package%E3%82%92import%E3%81%A7%E3%81%8D%E3%82%8B
> Python3.3以降の話。

PEP8 – import not at top of file with sys.path
https://stackoverflow.com/questions/36827962/pep8-import-not-at-top-of-file-with-sys-path