Jupyter・Colabのコードセルでテストするメモ

## 概要

Jupyter/ColabのPythonコードセルで標準のdoctestやunittestが動かせたのでメモとして残しておく。


## doctest

一般的にはメソッド内にdoctestを記述して`doctest.testmod()`を呼び出すことでテストを実行する仕組みですが、コードセル内でも同様のコードでテストすることができます。`vervose=True`を渡すとテストが成功した場合でも何をテストをしたのわかるので渡すようにしています。(Falseだと成功した場合は何も出力しません)

```python
def add(a, b):
    """
    >>> add(1, 2)
    3
    """

    return a + b

if __name__ == "__main__":
    import doctest
    doctest.testmod(verbose=True)
```

関数内にテストコードを書かずに別のセルに書く事もできます。一般的には別ファイルにテストコードを書いて`doctest.testfile()`で読み込んでテストするのですが、`doctest.run_docstring_examples()`を使えばテストコードをファイルではなく文字列で渡してテストすることができるようです。

```python
def add3(a, b, c):
    return a + b + c
```

```python
import doctest

teststr = """
>>> add3(1, 2, 3)
6
"""

doctest.run_docstring_examples(teststr, globals(), verbose=True)
```

最終的にPythonスクリプトを出力する前提で、出力したいコード部分を`%%writefile`マジックコマンドをつけたコードセルに書いておき、出力したPythonファイルに対して別のコードセルからテストを実行できると便利なのでやってみました。ただ単純にimportするだけだと更新前のコードをキャッシュしてコードの変更が反映されないので、ちょっと工夫する必要がありました。

```python
%%writefile add5.py

def sum5(a, b, c, d, e):
    return a + b + c + d + e
```

```python
import doctest
import sys
import importlib

sys.dont_write_bytecode = True

import add5
importlib.reload(add5)
from add5 import sum5


teststr = """
>>> sum5(1, 2, 3, 4, 5)
15
"""

doctest.run_docstring_examples(teststr, globals(), verbose=True)

```

`sys.dont_write_bytecode = True`はimportの際`__pycache__`ディレクトリにバイトコードを出力させないようにする設定です。変更の反映には影響なさそうですが、コーディング・テスト中にバイトコードを出力されてもしょうがないのでつけています。
`importlib.reload(add5)`を呼び出すことで変更したコード(モジュール)を再度読み直してくれて変更が反映されるようです。

Colabでも全く同じようにテストの実行が可能なので、確認用に用意してみました。

https://colab.research.google.com/github/uniras/Notebook/blob/main/doctest.ipynb


## unittest

unittest標準ライブラリでもdoctestと同じようにコードセルからテストができるようです。

一般的には`unittest.TestCase`を継承したテストクラスを定義して`unittest.main()`を呼び出すのですが、コードセルでも同様にして実行できます。ただし、コードセル上では引数として`argv`と`exit=False`を渡さないとエラーが出るようです。

```python
import unittest


def add(a, b):
    return a + b


class TestNotebook(unittest.TestCase):

    def test_add(self):
        self.assertEqual(add(2, 2), 4)


unittest.main(argv=[""], verbosity=2, exit=False)
```

普通に考えればそりゃそうだろうとは思いますが、一応テスト対象のコードとテストクラスを別々のコードセルに書くこともできます

```python
def add3(a, b, c):
    return a + b + c
```

```python
import unittest

class TestNotebook3(unittest.TestCase):

    def test_add3(self):
        self.assertEqual(add3(2, 2, 2), 6)

unittest.main(argv=["", "TestNotebook3.test_add3"], verbosity=2, exit=False)
```

引数`argv`に渡すリストの2番目に実行したいメソッドを渡すとそのメソッドだけテストを実行してくれます。何も指定しなかった場合は実行済みのコードセルに書いたテストクラスすべてをテストします。

同様に、別ファイルに出力したPythonファイルに対して別のコードセルからテストを実行することができます。

```python
%%writefile add6.py

def sum6(a, b, c, d, e, f):
    return a + b + c + d + e + f
```

```python
import unittest

import sys
import importlib

sys.dont_write_bytecode = True

import add6
importlib.reload(add6)
from add6 import sum6

class TestNotebook6(unittest.TestCase):

    def test_sum6(self):
        self.assertEqual(sum6(1, 2, 3, 4, 5, 6), 21)

unittest.main(argv=["", "TestNotebook6.test_sum6"], verbosity=2, exit=False)
```

こちらもColabでもテスト実行可能なので、用意しました

https://colab.research.google.com/github/uniras/Notebook/blob/main/unittest.ipynb

## 感想

Jupyterはコードの試行錯誤やテストにもってこいの環境であり、そこにテストライブラリを導入することによりさらにJupyter内でPythonコードの完成度を上げることができるので積極的に活用したい。