[思考中] high qualityなテストケースを書く方法

> この記事は思考実験です。いい案があれば教えてください。


あるメソッドのtest coverateを100%にするうまい方法を考える。
ただし、以下を最小限にしたい。
- `(1)テストの追加時のコスト` -> test case数は最小にしたい。
- `(2)デバッグ時のコスト` -> testの失敗時のデバッグを高速にできるようにしたい。-> それぞれのtest caseはシンプルであり、どうしてそれが落ちたのかが一目でわかるべき。
- `(3)テスト実行時のパフォーマンス` -> それぞれのtest caseにかかる実行時間は短くしたい。
- TODO: 他は？

## Tip1

実装の中でサブメソッドを切る。
そのサブメソッドは単体でテストする。

## Tip2

テストケースは単一目的にする。
このために、テストケースの追加時には、*何をテストしたいのか*をまず明確化する。
テストケースのdescriptionが書きにくい/分かりにくいのは、単一目的になっていないシグナルである。

> 何をテストしたいのか

これは、「ロジック内の分岐が正しいのか」を見たい場合もあれば、「エッジケースによりruntime errorが発生しないのか」を見たい場合もあるだろう。


## Tip3

テストケースのインプットは最小にする。
例えば、arrayを渡す場合、必要な要素のみ最小数にする。

## Tip4

そもそも、テストケースが多すぎる/複雑すぎる場合には、元の実装が良くないのでは？と疑うべき。
例えば、if文の数を減らせないか、処理の一部を別のメソッドに切り出せないか、を考えるべき。



# 例

## 例2 (Array)

[ref](https://leetcode.com/problems/can-place-flowers/description/?envType=study-plan-v2&envId=leetcode-75)

```py
# Returns True if a new flower can be planted on the given index in the flowerbed array.
# A new flower is plantable when the right/left adjacent spots and the given index have no flower. (Flower is represented as `1` in the flowerbed array).
def is_plantable(flowerbed, idx):
    if 1 <= idx and flowerbed[idx-1] == 1:
        return False
    
    if idx <= len(flowerbed) - 2 and flowerbed[idx+1] == 1:
        return False

    return flowerbed[idx] == 0
```

これのテストケースを書く。

```py
# General cases - check logic
assert is_plantable([1,0,0], 1) is False
assert is_plantable([0,0,1], 1) is False
assert is_plantable([0,1,0], 1) is False
assert is_plantable([0,0,0], 1) is True

# right end - check runtime error
assert is_plantable([0, 0], 0) is True
# left end - check runtime error
assert is_plantable([0, 0], 1) is True
```

気がついたことは、
- 先にGeneral casesのテストを書くべき。edge cases のテストより前に書く。
- 今回のedge case のテスト(= 左端/右端)で見たいのは、index out of range によるruntime error が発生しないこと。したがって、他の条件を満たしearly returnされることがないようにしたい。このために、test inputは、`flowerbed = [0,0]`にした。

## 例2 (Linked List)

これのテストを書くことを考えてみる。(Pseudo head を使う前提)
```py
# Remove a node on the given index in the given linked list mutably.
# If idx is out of range, it does nothing.
def remove_node(pseudo_head, idx):
    curr = pseudo_head
    for _ in range(idx):
        if curr.next is None: return
        curr = curr.next
    curr.next = curr.next.next
```

脳死で考えると、以下のテストケースを書いてしまう気がする。

```py
assert remove_node(Psuedo->11->22->33, 0) returns Psuedo->22->33 # remove head node
assert remove_node(Psuedo->11->22->33, 1) returns Psuedo->11->33 # remove middle node
assert remove_node(Psuedo->11->22->33, 2) returns Psuedo->11->22 # remove tail node
assert remove_node(Psuedo->11->22->33, 3) returns Psuedo->11->22->33 # not removed
```

これでも悪くはないが、もう少し頭を使ってみる。
コードを読んでそのカバレッジをあげることを意識すると、真のテスト観点は以下であることに気がつく。
- Test the traversal logic
- Test the linkage update logic
- Validate that out-of-index is not raised

では、前述したどのケースがどれをカバーしているのかを考えてみる。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/534442/b8a0984d-3b9c-2589-46b5-b1f3b3d19db5.png)

とすると、テストケースは *少なくとも* 以下があれば良い、ということになる。

```py
assert remove_node(Psuedo->11->22->33, 1) returns Psuedo->11->33 # remove middle node
assert remove_node(Psuedo->11->22->33, 3) returns Psuedo->11->22->33 # not removed
```

しかし、これは、前述した以下に反する。

> - `(2)デバッグ時のコスト`を下げたい
-> testの失敗時のデバッグを高速にできるようにしたい
-> それぞれのtest caseはシンプルであり、どうしてそれが落ちたのかが一目でわかるべき。

どういうことか説明する。
もし`remove first node`, `remove middle node`, `remove last node` のテストケースがあり、実行結果が以下だったとする。

- [pass] `remove first node`
- [fail] `remove middle node`
- [fail] `remove last node`

これを見れば、冴えたエンジニアなら、「traversalのロジックがバグってそう」と気がつけるだろう。

結論。まず真のテスト観点を考えた上で、テストケースを必要性を理解した上で増やす。