Python 3 エンジニア認定実践試験対策 (7) - doctest

Pythonを数年使ってきて、改めて知識の整理と最新仕様のキャッチアップのために Python 3 エンジニア認定実践試験を受験しようと思い、私的に作成した資料から簡易版を作成しました。
最後に練習問題を設けましたので、解説を読んだら記憶定着のために練習問題へチャレンジしてください。最後に答えを掲載しています。

---

## doctestとは

- **目的**: 
  - Pythonの標準ライブラリの一部で、ドキュメント内に記述したコードサンプルをテストするためのシンプルなフレームワークです。
  - ドキュメントとコードの整合性を保ち、品質管理に役立ちます。
  - 基本的にはdocstring内に1行〜数行の簡単なテストを書く用途で使用されます。

- **利点**: 
  - 簡単に実装でき、既存のコードに容易に組み込むことができます。
  - 対話モードのコードを使用するため、実行結果を直接確認できます。

---

## doctestの基本的な書き方

1. **docstringコメントの中にサンプルコードを記述**
   - テストコードは対話モードのプロンプトに似た形式で書きます。
     - `>>>`：テストコードの行の先頭に付けます。
     - `...`：1つのテストが複数行にわたる場合、2行目以降に付けます。

   ```python
   def add_numbers(x, y):
       """
       2つの数を加算する関数です。

       >>> add_numbers(4, 5)
       9
       >>> a = 3
       ... b = 7
       ... add_numbers(a, b)
       10
       """
       return x + y
   ```

2. **予想される出力を記述**
   - テストコードの下に、期待される出力を記述します。

3. **空白行の扱い**
   - 出力結果に空白行が含まれる場合や戻り値として何も出力しない場合は、`<BLANKLINE>`を明示的に使用して表現する必要があります。

   ```python
   def no_output_function():
       """
       何も出力しない関数です。

       >>> no_output_function()
       <BLANKLINE>
       """
   ```

---

## doctestの実行方法

1. **コマンドラインからの実行**
   ```bash
   python -m doctest -v your_module.py
   ```
   - テストが成功している場合は結果を何も表示しないため、`-v`オプションを付けることで、テスト結果の詳細を表示します。

2. **doctest.testmod()関数を使用**
   - モジュール内でテストを実行したい場合は、`doctest`モジュールをインポートし、`doctest.testmod()`を呼び出します。

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

---

## 例外のテスト

- 関数が例外を正しく処理するかどうかをテストできます。
- doctestは、例外のログ（トレースバック）を文字列検索して、例外が発生したかどうかを検出します。
- 期待するテスト結果には、ログに出力されるであろう文言や例外名を文字列で記述しておき、doctestに文字列マッチングさせます。

### サンプルコード

```python
def safe_divide(numerator, denominator):
    """
    2つの数を割る関数です。

    >>> safe_divide(10, 2)
    5.0
    >>> safe_divide(10, 0)
    Traceback (most recent call last):
        ...
    ZeroDivisionError: division by zero
    """
    return numerator / denominator
```

---

## 例外テストの記述方法

1. **トレースバックヘッダーを書く**  
   実際に例外が発生したときのエラーメッセージの最初の部分を書き、検索にマッチさせます。
   ```
   Traceback (most recent call last):
   ```

2. **省略記号（...）**  
   トレースバックヘッダーと最後に指定する例外名との間には、その時々で異なる情報が表示されます。この部分をスキップさせるために、`...`を使って、異なるエラー内容の詳細をスキップさせて、例外名だけでマッチさせることが可能になります。  
   `...`を使わずに期待される出力を完全に記述することも可能ですが、この場合、トレースバックの詳細が完全に一致しない限り、テストは失敗します。

3. **例外名**  
   発生する例外のクラス名とそのメッセージを記述します。例えば、`ZeroDivisionError: division by zero`のように書きます。

---

## ネストされた例外の検出

例外が入れ子になっている場合、`Traceback (innermost last):`を使うと、最も内側の例外を指定できます。

```python
def nested_exception_function():
    """
    入れ子になった例外を発生させる関数です。

    >>> nested_exception_function()
    Traceback (innermost last):
        ...
    ValueError: invalid value
    """
```

---

## テストを外部ファイルに分離する

doctestはdocstring内に書かないと動作しないというわけではありません。docstring以外でも、様々な利用方法があります。以下は、外部ファイルに分離して利用する方法です。

- **doctest.testfileを使用**  
  テストケースを外部ファイルに記述し、そのファイルからテストを実行できます。

  ```python
  import doctest

  if __name__ == "__main__":
      doctest.testfile('test_cases.txt')
  ```

- **外部ファイルの例**  
  `test_cases.txt`に以下のように記述します。

  ```
  >>> from example import safe_divide
  >>> safe_divide(10, 2)
  5.0
  >>> safe_divide(10, 0)
  Traceback (most recent call last):
      ...
  ZeroDivisionError: division by zero
  ```

---
# 確認問題

1. doctestの主な目的は何ですか？
   a) Pythonのプログラムを最適化すること  
   b) ドキュメント内のコードサンプルをテストすること  
   c) Pythonのパッケージをインストールすること  
   d) Pythonのバージョンを管理すること  

2. doctestを使用する利点はどれですか？
   a) コードの実行速度を向上させる  
   b) 簡単に実装でき、既存のコードに組み込みやすい  
   c) 複雑なテストフレームワークを必要とする  
   d) コードの可読性を低下させる  

3. doctestでテストコードを書く際、どのような形式で記述しますか？
   a) `#`で始まるコメント形式  
   b) `>>>`で始まる対話モード形式  
   c) `//`で始まるJavaスタイルのコメント形式  
   d) `/* */`で囲む形式  

4. doctestの実行方法として正しいものはどれですか？
   a) `python -m test`  
   b) `python -m doctest -v your_module.py`  
   c) `doctest.run(your_module.py)`  
   d) `python test.py`  

5. 期待される出力に空白行が含まれる場合、どのように表現しますか？
   a) `<EMPTY>`  
   b) `<BLANKLINE>`  
   c) `""`  
   d) `None`  

6. 例外をテストする際、doctestで必要な記述はどれですか？
   a) 例外の発生を無視する  
   b) 例外のトレースバックを完全に記述する  
   c) トレースバックヘッダーと例外名を記述する  
   d) 例外の詳細を省略する  

7. 最も最近の呼び出しで発生している例外をテストで捕捉する際、トレースバックヘッダーはどのように記述しますか？
   a) `Traceback (most recent call last):`  
   b) `Traceback (innermost last):`  
   c) `Traceback (outermost last):`  
   d) `Traceback (last):`  

8. ネストされた例外をテストする際、トレースバックヘッダーはどのように記述しますか？
   a) `Traceback (most recent call last):`  
   b) `Traceback (innermost last):`  
   c) `Traceback (outermost last):`  
   d) `Traceback (last):`  

9. doctestを外部ファイルに分離する方法として正しいものはどれですか？
   a) `doctest.testfile('test_cases.txt')`  
   b) `doctest.load('test_cases.txt')`  
   c) `doctest.runfile('test_cases.txt')`  
   d) `doctest.execute('test_cases.txt')`  

10. docstring内に記述するテストコードの形式で、複数行にわたる場合に使う記号は何ですか？
    a) `...`  
    b) `>>`  
    c) `#`  
    d) `*`  

11. doctestを実行する際、`-v`オプションを付ける理由は何ですか？
    a) テストをスキップするため  
    b) テスト結果の詳細を表示するため  
    c) テストを非表示にするため  
    d) テストの実行時間を短縮するため  

12. `no_output_function`の期待される出力は何ですか？
    a) `None`  
    b) `<BLANKLINE>`  
    c) `0`  
    d) `Error`  

13. doctestで例外名を記述する際、どのように書きますか？
    a) `Error: message`  
    b) `Exception: message`  
    c) `ZeroDivisionError: division by zero`  
    d) `DivisionError: by zero`  

14. doctestのテストコードはどのように実行されますか？
    a) 自動的に実行される  
    b) 手動で実行する必要がある  
    c) コードの最初に書く必要がある  
    d) 特別な設定が必要である  

15. doctestを使用して、どのようにモジュール内のテストを実行しますか？
    a) `doctest.run()`を使用する  
    b) `doctest.testmod()`を使用する  
    c) `doctest.execute()`を使用する  
    d) `doctest.check()`を使用する  

解答:
1. b  
2. b  
3. b  
4. b  
5. b  
6. c  
7. a 
8. b  
9. a  
10. a  
11. b  
12. b  
13. c  
14. b  
15. b