pythonのunittestについてまとめる

今回のお題
-
今回はpythonの`unittest`についてまとめます。

`unittest`とはpythonの標準モジュールの一つであり、ここにはテストコードの実行に必要なクラスや関数がまとめられています。

すなわちこのモジュールをimportすることで手軽にテストコードの実装が可能になるというわけです。

本記事で扱うこと
-
- unittestの基本的な仕組み
- unittestの実行方法
- unittestの結果の見方

本記事で扱わないこと
-
- DOM操作関連
- パラメータテスト
- テストで用いるDBの設定方法

アプリケーションにテストを実装するとなるとDBの設定などでややこしくなるので、まずはテストのスクリプト単体で動作させることを目指します。

また、パラメータテストについてはパッケージのインストールが必要なので別で取り上げます。

unittestの仕組み
-
unittestの仕組みをごく単純化すると、

- テストコードが書かれたファイルを用意する
- そのファイルを`% python3 ファイル名.py`として実行する

これだけです。

そしてテストコードファイルの中身はこんな感じ。

```python:tests.py
# unittestモジュールのimport
import unittest

# TestCaseクラスの定義
class MyTestCase(unittest.TestCase):
  # テスト実行関数の定義
  def test_test1(self):
    print("hoge")

# テストの実行判定
if __name__ == "__main__":
    unittest.main()
```

基本的には`unittest.TestCase`を継承したクラスを作成し、その中で`test_xxx`というインスタンスメソッドを定義します。

この`test_xxx`がテストの実行関数であり、`% python3 tests.py`でテストを実行した際に呼び出されます。

最後の

```python
if __name__ == "__main__":
  unittest.main()
```
についてですが、`unittest.main()`はモジュール内の全ての`test_xxx`メソッドを呼び出して実行する役割、`if __name__ == "__main__"`はこのモジュールが他のファイルにimportされた際には`unittest.main()`が実行されないようにするための役割です。

今回は詳しい説明は省きますが、おまじないだと思って書いてください。


さて、上記のtests.pyを実行しても今回は`hoge`と出力されるだけです。

実行関数の中身が`print("hoge")`なので当たり前ですね。
  
実際にはこのテスト実行関数の中身を、プログラムが期待通りに動いているかどうかを調べるメソッドにすることでテストを行います。

テスト用のメソッド達
-

### assertEqual(a, b)
`a == b`であることを確かめる関数。

```python
import unittest

class MyTestCase(unittest.TestCase):
  def test_test1(self):
    self.assertEqual((10 + 5), 15) # assertEqualはTestCaseクラスの関数なのでself.の形で呼び出す。

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

### assertNotEqual(a, b)
`a != b`であることを確かめる関数。

### assertTrue(a)
`a == True`であることを確かめる関数。

### assertFalse(a)
`a == False`であることを確かめる関数。

### assertIn(a, b)
aがbに含まれるかどうかを調べる関数。

aとbの関係性は`文字列対文字列`, `文字列対リスト`, `リスト対リスト`のいずれでも可(辞書は使えない。成功失敗判定の例は以下参照）。

```python
class MyTestCase(unittest.TestCase):
  def test_test3(self):
    self.assertIn("hoge", "hogehoge") # これはOK
  
  def test_test4(self):
    self.assertIn("hoge", ["hoge", "fuga"]) # これもOK

  def test_test5(self):
    # self.assertIn(["hoge", "fuga"], ["hoge", "fuga", "fuga"]) # これはNG
    # self.assertIn(["hoge", "fuga"], ["hoge", "fuga"]) # これでもNG
    pass
  
  def test_test6(self):
    self.assertIn(["hoge", "fuga"], [["hoge", "fuga"] ,["hoge", "hoge", "fuga", "fuga"]]) # これはOK
  
  def test_test7(self):
    self.assertIn(("hoge", "fuga"), (("hoge", "fuga"), ("hoge", "hoge", "fuga", "fuga"))) # これもOK
```

### with self.assertRaises
-
引数として与えた例外の有無を確認する。

```python
class MyTestCase(unittest.TestCase):
  def test_div(self):
    with self.assertRaises(ZeroDivisionError):
      return (5 / 0) # これはテストOK
      # return (5 / 3) これは失敗
```

setUpとtearDown
-
setUp系メソッドはテスト実行前、tearDown系メソッドはテスト実行後に呼び出される。

呼び出しのタイミングはモジュールの呼び出し前後、クラスの呼び出し前後、メソッドの呼び出し前後の計６通り。

詳細は以下。

| 関数名 | 実行タイミング | 関数の種類 |
|:-:|:-:|:-:|
| setUp | テスト関数実行前 | インスタンスメソッド |
| tearDown | テスト関数実行後 | インスタンスメソッド |
| setUpClass | クラスの呼び出し前 | クラスメソッド |
| tearDownClass | クラスの呼び出し後 | クラスメソッド |
| setUpModule | モジュールの呼び出し前 | クラスとは独立 |
| tearDownModule | モジュールの呼び出し後 | クラスとは独立 |

```python
import unittest
from unittest import TestCase

def setUpModule():
  print("set up module")

def tearDownModule():
  print("tear down module")

class MyTestCase1(TestCase):
  @classmethod
  def setUpClass(cls) -> None:
    print("set up class1")
    return super().setUpClass()

  @classmethod
  def tearDownClass(cls) -> None:
    print("tear down class1")
    return super().tearDownClass()

  def test_test1(self):
    print("test1")
  
  def test_test2(self):
    print("test2")

  def setUp(self) -> None:
    print("set up")
    return super().setUp()

  def tearDown(self) -> None:
    print("tear down")
    return super().tearDown()

class MyTestCase2(TestCase):
  @classmethod
  def setUpClass(cls) -> None:
    print("set up class2")
    return super().setUpClass()

  @classmethod
  def tearDownClass(cls) -> None:
    print("tear down class2")
    return super().tearDownClass()

  def test_test1(self):
    print("test1")
  
  def test_test2(self):
    print("test2")

  def setUp(self) -> None:
    print("set up")
    return super().setUp()

  def tearDown(self) -> None:
    print("tear down")
    return super().tearDown()

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

実行結果は以下。

```shell
set up module
set up class1
set up
test1
tear down
.set up
test2
tear down
.tear down class1
set up class2
set up
test1
tear down
.set up
test2
tear down
.tear down class2
tear down module
```
`.`がついている部分は、直前にテスト実行関数が成功していることを示しています。

@unitest.skip
-
直後のテストをスキップする。

スキップの範囲はこの記述を配置した場所によって決まり、関数の前においたのであればその関数（及びその前後のsetUpメソッド、tearDownメソッド）のみを、クラスの前においたのであればそのクラス（と付随するsetUpメソッド、tearDownメソッド、setUpClassメソッド、tearDownClassメソッド）全体をスキップする。

```python:使用例
# @unittest.skip("not implemented yet")
class MyTestCase2(TestCase):
  @classmethod
  def setUpClass(cls) -> None:
    print("set up class2")
    return super().setUpClass()

  @classmethod
  def tearDownClass(cls) -> None:
    print("tear down class2")
    return super().tearDownClass()

  @unittest.skip("not implimented yet") # ここをスキップ
  def test_test1(self):
    print("test1")
  
  def test_test2(self):
    print("test2")

  def setUp(self) -> None:
    print("set up")
    return super().setUp()

  def tearDown(self) -> None:
    print("tear down")
    return super().tearDown()

if __name__ == "__main__":
  unittest.main()
```
()内にはスキップの理由を記載することが可能。

また、テスト実行結果のログにおいてはスキップ部分は`s`に置き換わる。

```shell:テスト結果
set up module
set up class2
sset up # ここの先頭のsがスキップ箇所
test2
tear down
.tear down class2
tear down module
```

また、テスト実行時に`-v`をオプションでつけると詳細なログが表示され、その際には`@unittest.skip()`の括弧内のスキップ理由も表示される。

```shell
% python3 testcase.py -v

# 結果
set up module
set up class2
test_test1 (__main__.MyTestCase2) ... skipped 'not implimented yet'
test_test2 (__main__.MyTestCase2) ... set up
test2
tear down
ok
tear down class2
tear down module

----------------------------------------------------------------------
Ran 4 tests in 0.000s

OK (skipped=1)
```