Python の unittest ライブラリの基本

## はじめに

Python でのテスト駆動開発（TDD）やユニットテストには、標準ライブラリの `unittest` が使用できます。
ユニットテストは品質の高いコードを維持するための重要な手法の一つです。
本記事では、`unittest` の基本的な使い方にフォーカスして、実践的な例を交えて解説します。

## テストターゲット

単純な計算を行うクラスを対象にします。

```python
class Calculator:
    """基本的な算術演算を行うクラス

    このクラスは単純な計算機能を提供し、テストの例示に使用します。
    """

    def add(self, x, y):
        """2つの数値を加算します"""
        return x + y

    def divide(self, x, y):
        """除算を行います。ゼロ除算時は ValueError を発生させます"""
        if y == 0:
            raise ValueError("Cannot divide by zero")
        return x / y
```

このクラスが正常に動作するかテストするコードは、どのように記述すれば良いでしょうか ?

## add メソッドのテストを考える

まず、add メソッドが正常に動作するか確認することを考えてみます。
下記のコードはいかがでしょうか ?

```python
import unittest

class TestCalculator(unittest.TestCase):
    def setUp(self):
        self.calc = Calculator()

    def test_add(self):
        result = self.calc.add(3, 5)
        self.assertEqual(result, 8, "3 + 5 should equal 8")
```

### unittest 使用時の基本

`unittest.TestCase` クラスを継承したクラスにテストを記述します。
`test_add()` メソッドが実際のテストを行うメソッドで、特にこのメソッドを具体的に呼び出す処理は記述せずにテストが実行できます。

### assertEqual() による等価性評価

add メソッドは、2 つの引数の値を加算した結果を返すので、テスト用の引数値を検討して結果を比較してあげれば良いです。
この実装だと、`result` に `8` が返ればテスト成功、そうでない場合はテスト失敗となり、失敗時のメッセージとして `3 + 5 should equal 8` が出力されます。

### setUp() と tearDown()

`setUp()` メソッドは、テスト実行時に一連のテストメソッドが実行され始める前に一度だけ呼ばれるメソッドです。

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

Calculator クラスのインスタンスは何度も生成する必要がないため、`setUp()` メソッド内で生成しています。
この例だとあまり意味が無いですが、例えばデータベースとの接続を確立していくつかテストを実施する場合など、重複して実行させたくない比較的重たい初期化処理が必要な際に利用すると良いです。

例には記載していませんが、`tearDown()` メソッドを記述することで一連のテストメソッドが全て終了した後に一度だけ呼ばれるメソッドを定義できます。

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

例えばデータベースとの接続をクローズする処理が必要な際に利用すると良いです。

## divide メソッドのテストを考える

次に、divide メソッドが正常に動作するか確認することを考えてみます。
下記のコードはいかがでしょうか ?

```python
def test_division_by_zero(self):
    # 0 除算がエラーになるかを検証
    with self.assertRaises(ValueError) as context:
        self.calc.divide(10, 0)

    # 例外メッセージの検証
    self.assertTrue("Cannot divide by zero" in str(context.exception))
```

割り算の場合、0 除算をしようとすると例外が発生しますので、例外発生時の振舞いをテストできるように備えます。

### assertRaises の使い方

`assertRaises` は、特定の処理が期待通りの例外を発生させることを確認するために使用します。

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

この例のようにコンテキストマネージャとして使うと、exception で指定されたオブジェクトを格納できます。
今回だと `ValueError` のオブジェクトが `context` に入ります。
得られたオブジェクトを使用して、追加の検証を行うこともできます。

## メソッドに対して、複数の入力パターンをテストすることを考える

add メソッドについて、マイナスの値や大きな値でも動作するか検証したいとき、1 つのテストメソッド内にまとめて `assertEqual()` を書きたくなります。

```python
def test_add(self):
    result = self.calc.add(3, 5)
    self.assertEqual(result, 8, "3 + 5 should equal 8")

    result = self.calc.add(-1, -1)
    self.assertEqual(result, -2, "-1 + (-1) should equal -2")

    result = self.calc.add(100, 200)
    self.assertEqual(result, 300, "100 + 200 should equal 300")
```

しかし、こうすると、もし最初の `assertEqual()` でテスト失敗となると、後続のテストが実行されません。
そうすると、一度のテストで検出できる結果が限定されてしまい、テスト効率の悪化につながる恐れがあります。

何か打つ手はあるでしょうか ?
こちらは下記のように記載することで、全ての `assertEqual()` が評価されるようにできます。

```python
def test_multiple_additions(self):
    test_cases = [
        (3, 5, 8),
        (-1, 1, 0),
        (100, 200, 300)
    ]

    for x, y, expected in test_cases:
        with self.subTest(x=x, y=y, expected=expected):
            result = self.calc.add(x, y)
            self.assertEqual(result, expected)
```

### subTest による複数の検証の実施

`subTest`を使用すると、1 つのテストメソッド内で複数のケースをまとめて検証できます。

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

テスト失敗時は、引数に記載した内容がメッセージとして出力されます。

```bash
FAIL: test_multiple_additions (main.TestCalculator.test_multiple_additions) (x=3, y=5, expected=8)
----------------------------------------------------------------------
Traceback (most recent call last):
  File "/Users/***/main.py", line 43, in test_multiple_additions
    self.assertEqual(result, expected)
    ~~~~~~~~~~~~~~~~^^^^^^^^^^^^^^^^^^
AssertionError: 9 != 8
```

## テストの実行と出力の確認

ここまでの内容を踏まえて、クラス定義とテストの実装の全体は下記になります。

```python:main.py
import unittest


class Calculator:
    """基本的な算術演算を行うクラス

    このクラスは単純な計算機能を提供し、テストの例示に使用します。
    """

    def add(self, x, y):
        """2つの数値を加算します"""
        return x + y

    def divide(self, x, y):
        """除算を行います。ゼロ除算時はValueErrorを発生させます"""
        if y == 0:
            raise ValueError("Cannot divide by zero")
        return x / y


class TestCalculator(unittest.TestCase):
    def setUp(self):
        self.calc = Calculator()

    def test_add(self):
        result = self.calc.add(3, 5)
        self.assertEqual(result, 8, "3 + 5 should equal 8")

    def test_division_by_zero(self):
        # 0 除算がエラーになるかを検証
        with self.assertRaises(ValueError) as context:
            self.calc.divide(10, 0)

        # 例外メッセージの検証
        self.assertTrue("Cannot divide by zero" in str(context.exception))

    def test_multiple_additions(self):
        test_cases = [(3, 5, 8), (-1, 1, 0), (100, 200, 300)]

        for x, y, expected in test_cases:
            with self.subTest(x=x, y=y, expected=expected):
                result = self.calc.add(x, y)
                self.assertEqual(result, expected)

```

テストは下記のように、python コマンドに `-m unittest` を指定することで実行できます。
上記のコードを `main.py` に記述している場合は、下記のようになります。

```bash
python -m unittest main.py
```

実行結果の例：

```bash
$ python -m unittest main.py
----------------------------------------------------------------------
Ran 3 tests in 0.000s

OK
```

`-v` オプションを指定することで、どのテストメソッドが実行されたかも表示されます。

```bash
python -m unittest -v main.py
```

実行結果の例：

```bash
$ python -m unittest -v main.py
test_add (main.TestCalculator.test_add) ... ok
test_division_by_zero (main.TestCalculator.test_division_by_zero) ... ok
test_multiple_additions (main.TestCalculator.test_multiple_additions) ... ok

----------------------------------------------------------------------
Ran 3 tests in 0.000s

OK
```

## まとめ

Python の `unittest` ライブラリは、効果的なユニットテストを作成するための豊富な機能を提供しています：

- `assertEqual` で値の等価性を検証
- `assertRaises` で例外の発生を確認
- `subTest` で複数のテストケースを効率的に実行
- `setUp`/`tearDown` でテストの前処理・後処理を管理

これらの機能を適切に組み合わせることで、メンテナンス性が高く信頼できるテストスイートを構築できます。