Myersの三角形問題でテストの観点を養う

京セラコミュニケーションシステムの要谷（かなめや）です。

前回からかなり時間が空いてしまいましたが、今回は実際に『テストする時の観点』についてMyersの三角形問題を利用して執筆しています。

# 対象の読者
今回の記事は実際にテストするときの観点をイメージしたい人や、単体テスト（ユニットテスト）の際に異常系をどのように確認すればよいのか迷う方をターゲットにしています。

# Myers（マイヤーズ）の三角形問題
以前、社内でソフトウェアテストについての講師をした際、導入の部分でMyersの三角形問題を使いましたので、そちらについて説明したいと思います。
内容は[ASTERセミナー標準テキスト](https://www.aster.or.jp/business/seminar_text.html)からの抜粋です。

:::note info
問題
以下のプログラムをテストするのに十分と思われる一連のテスト内容（何を確認するのか？）を書いてください。
1. あるプログラムでは3つの整数を入力することができます
1. それぞれの値は三角形の各辺の長さです
1. プログラムでは入力値に応じて **正三角形**、**二等辺三角形**、**不等辺三角形**（3つの辺がいずれも等しくない三角形）のどれかであるメッセージを表示します
:::

この問題は、単純なプログラムであってもテストを抜け漏れなく検討することは難しいという例です。
せっかくですので、皆さんも少しだけでも考えてみてください。

# 解答例と解説

Myersの三角形問題の一般的な確認項目は以下の通りとされています。
皆さんはいくつ思いついたでしょうか。

| 確認項目 | 入力値 | 期待する結果 |
|:--|:--|:--|
| ①有効な正三角形の場合 | 2,2,2 | 正三角形 |
| ②有効な不等辺三角形の場合 | 3,4,5 | 不等辺三角形 |
| ③有効な二等辺三角形の場合 | 5,5,3 | 二等辺三角形 |
| ④有効な二等辺三角形で、各辺の組み合わせを考慮する | 3,5,5 or 5,3,5 | 二等辺三角形 |
| ⑤ある辺が0の場合 | 0,1,2 | 三角形ではない |
| ⑥3辺すべてが0の場合（正三角形と表示されないこと） | 0,0,0 | 三角形ではない |
| ⑦ある辺が負の値の場合 | -1,2,3 | 三角形ではない |
| ⑧2辺の和が残りの1辺と等しい場合 | 1,2,3 | 三角形ではない |
| ⑨2辺の和が残りの1辺と等しい場合で、各辺の組み合わせを考慮する | 2,1,3 or 2,3,1| 三角形ではない |
| ⑩2辺の和が残りの1辺よりも小さい場合 | 1,1,3 | 三角形ではない |
| ⑪2辺の和が残りの1辺よりも小さい場合で、各辺の組み合わせを考慮する | 1,3,1 or 3,1,1 | 三角形ではない |
| ⑫整数ではない入力値の場合 | **1.5**,2,3 | 入力値異常 |
| ⑬入力する数が3つではない場合（2つ以下、4つ以上の場合） | 1,2 or 1,1,1,1 | 入力値異常 |
| ⑭結果を表示すること | ー | ー |

エンジニアの平均では7つから8つとのことですので、半分ほどの項目は意図せず見落としている可能性があります。

また、上記だけが正解というわけではなく、
- 入力値の桁あふれが起こらないか
- 数値ではなく文字を与えられないか
- 分数や式を与えられないか

といった観点を思いついた方がいらっしゃるかもしれません。レビューアの目線としては重要な部分だと思います。

# ChatGPTに聞いてみた
参考までにMyersの三角形問題とそのテストコードについてChatGPTにPythonで書いてもらいました。実行時点でGPT-4oです。
前出の観点よりもテストケースが不足していることがわかります。何が足りてないか探してみてください。
皆さんがレビューする際は『仕様に対して抜け漏れがないか』と併せて、『どんな異常が起こりそうで、異常ケースの見落としが無いか』という目を持つことが大切です。

``` classify_triangle.py
def classify_triangle(a, b, c):
    # Check for valid triangle
    if a <= 0 or b <= 0 or c <= 0:
        return "Invalid"
    if a + b <= c or a + c <= b or b + c <= a:
        return "Invalid"

    # Classify the triangle
    if a == b == c:
        return "Equilateral"
    elif a == b or b == c or a == c:
        return "Isosceles"
    else:
        return "Scalene"

# Test cases
test_cases = [
    (3, 3, 3, "Equilateral"),
    (3, 3, 2, "Isosceles"),
    (3, 4, 5, "Scalene"),
    (1, 1, 3, "Invalid"),
    (0, 4, 5, "Invalid"),
    (-1, 2, 3, "Invalid"),
    (1, 1, 1, "Equilateral")
]

for i, (a, b, c, expected) in enumerate(test_cases):
    result = classify_triangle(a, b, c)
    assert result == expected, f"Test case {i+1} failed: ({a}, {b}, {c}) => {result} (expected: {expected})"
    print(f"Test case {i+1} passed: ({a}, {b}, {c}) => {result}")
```

以下は実行結果です。
```　print.ex
Test case 1 passed: (3, 3, 3) => Equilateral
Test case 2 passed: (3, 3, 2) => Isosceles
Test case 3 passed: (3, 4, 5) => Scalene
Test case 4 passed: (1, 1, 3) => Invalid
Test case 5 passed: (0, 4, 5) => Invalid
Test case 6 passed: (-1, 2, 3) => Invalid
Test case 7 passed: (1, 1, 1) => Equilateral
``` 
# まとめ
実装する際、正常系はユーザの要求から仕様を考えることが多いためテストケースも検討しやすいのですが、異常系に関しては実装経験や担当するドメインでの業務経験が必要になってくることも多いかと思います。

今回用いたMyersの三角形問題が皆さんのテストにおける観点に対しての気づきとなれば幸いです。

# 所属部署について
### ITエンジニアが活躍する地方拠点「長崎 Innovation Lab」
https://www.kccs.co.jp/contents/nagasaki-innovation-lab/

長崎 Innovation Labでは、IT技術を活用して、工場などものづくりの現場で活用できるシステムの開発に取り組んでいます。自由な発想でこれまでにない社会に役立つ製品・サービスを生み出し、長崎から発信していくことを目指しています。