改めてテスト技法についてまとめてDjangoにどう適用するか考えてみた

morimorikochanです、 最近スプラトゥーン3を売りました

社内の勉強会でこのテーマで発表しました。
markdownからスライドを生成するサービスを利用して、この記事の元になっているmarkdownからスライドを作成して発表しました。

せっかくなのでmarkdownのまま以下に保存しておきます


Djangoを書いてて

「あれ、テストって何を書けばええんや」

「これでちゃんとテストできてるんか?」

ってことないですか?


そこで今日は

  1. 一般的なテストの種類について紹介
  2. 社内で利用しているDjangoではどのようなことに注意してテストを実装すれば良いか

の2点を紹介したいと思います


そもそもテストって何? その1

IEEE 標準規格 610.12-1990の “IEEE Standard Glossary of Software Engineering TermiNology"によると

ある特定の条件下でシステムまたはコンポーネントを操作するプロセスであり、その結果を観察または記録して、システムまたはコンポーネントのある側面を評価すること


そもそもテストって何? その2

「体系的ソフトウェアデザイン」によると

テストとは、テストされるソフトウェアの品質を測定して改善するために、テストウェアをエンジニアリングし、利用し、保守しながら、同時並行的にすすめるライフサイクルプロセスである

開発時のことだけでなく、保守時のことまで考慮されていて、ソフトウェアの重要なプロセスとして捉えられている


そんな大事なんやったら全パターンテストすればええやん?

現実的ではないです

  • 例1) 生年月日から年齢を返す関数
    • パターン数は100*365=3650000
  • 例2) 文字列で表現された数値を小数点2桁で四捨五入する関数
    • パターン数は無限大

いかに効果的なテストケースを選ぶかが大事


テストを大きく分けると

ブラックボックステスト

  • 実装の詳細を考慮せず行われるテスト
  • 実装内部のロジック・依存関係・構造に関する知識を必要としない
  • 今日のメインはこっち
  • [個人的な意見] テストコードは実装の詳細に関与せずインタフェースのみに関与すべきと考えているのでホワイトボックステストよりこちらをよく書きます


ホワイトボックステスト

  • 実装の詳細を考慮して行われるテスト
  • 実装内部のロジック・依存関係・構造に関する知識を必要とする

1.同値クラステスト

同じ挙動をする範囲(同値クラス)から1つのみを選びテストする

  • 単一の入力に対して適用する
  • ポピュラーなテスト技法
  • 例えば「年齢(整数)を渡すとお酒が飲めるかどうかを返す関数」の場合
    • お酒が飲めない年齢の同値クラスは0~19歳、
    • お酒が飲める年齢の同値クラスは20歳~
    • 同値クラステストとしては以下の2パターンをテストすれば満たす
      • 15歳(お酒が飲めない)
      • 25歳(お酒が飲める)

1.同値クラステスト

同じ挙動をする範囲(同値クラス)から1つのみを選びテスト

  • 引数が連続している場合は、同値クラスが3つになることもある
  • 例えば「小児用の新型コロナワクチンを打つことができるかどうかを返す関数」の場合
    • 0~4歳で1つの同値クラス(打てない)
    • 5~11歳で1つの同値クラス(打てる)
    • 12歳~で1つの同値クラス(打てない)

1.同値クラステスト in Django

以下のようなModelに対するPOST(create)のエンドポイントには

class Movie(models.Model):
    description = models.TextField(blank=False, null=True, max_length=255)

リクエストBodyとして以下のような同値クラスのテストケースが考えられる。

  • {}
  • {'description': null}
  • {'description': ''}
  • {'description': '1234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890'}
  • {'description': 'awesome movie!!!'}

nullやkeyがない場合も忘れずにテストしましょう


2.境界値テスト

同値クラスの境界をテストする技法

  • 単一の入力に対して適用する
  • 各境界ごとに2つずつもしくは3つずつテストケースを作る
  • 例えば「小児用の新型コロナワクチンを打つことができるかどうかを返す関数」の場合
    • 4歳
    • 5歳
    • 11歳
    • 12歳

2.境界値テスト

同値クラスの境界をテストする技法

  • 各境界ごとに2つずつもしくは3つずつテストケースを作る
    • 2つずつの場合は2値の選択2ポイント境界値分析と呼ばれ、有効値と無効値を1つずつ選択する
    • 3つずつの場合はは3値の選択3ポイント境界値分析と呼ばれ、有効値1つ・無効値2つもしくは有効値2つ・無効値1つ
  • どちらを選ぶかは諸説ある

2.境界値テスト in Django

以下のようなModelに対するPOST(create)のエンドポイントには

class Movie(models.Model):
    score = models.FloatField(validators=[
            MaxValueValidator(100),
            MinValueValidator(0)
    ])

リクエストBodyとして以下のような同値クラスのテストケースが考えられる

  • {'score': -0.000001}
  • {'score': 0}
  • {'score': 0.0000001}
  • {'score': 99.999999}
  • {'score': 100}
  • {'score': 100.0000001}

効果的ではないテストケースを選ばないよう気をつけましょう。 {'score': -1}{'score': 1}のようなテストケースは効果的ではないです


3.デシジョンテーブルテスト

  • 複数の入力に対して適用する
  • デシジョンテーブルとは?

デシジョンテーブルは、条件の集合に基づいて、複雑なビジネスルールを表現するもの


3.デシジョンテーブルテスト

例えば、とある映画館のチケット料金割引システムの仕様は以下の通り

  • 家族2人以上で購入した場合は250円割引
  • 学生が購入した場合は500円割引
  • 上記2つとも満たす場合は600円割引
  • レイトショーのチケットを購入した場合は800円割引

3.デシジョンテーブルテスト

デシジョンテーブルではこのように表現される

  • x軸にはパターン・ルールを
  • y軸には入力(=条件)と出力(アクション)を

3.デシジョンテーブルテスト

デシジョンテーブルテストでは、このx軸がそのままテストケースになる


3.デシジョンテーブルテスト おまけ

よくみると、レイトショーがYesの場合は、他の条件がなんであれ必ず割引料金が800円になってる…

こういうような場合は他の条件をDCとして表現し、複数のテストケースを1つで賄うことができる


3.デシジョンテーブルテスト in Django

@pytest.mark.parametrizeを使って、デシジョンテーブルをそのまま表現できるようにしよう ※行列の関係が逆なのでややこしい

@pytest.mark.parametrize("is_in_late_show, is_with_family, is_student, discount_price", [
    (True, True, True, 800),
    (False, True, True, 600),
    (False, True, False, 250),
    (False, False, True, 500),
    (False, False, False, 0)
])
def test_discount_price(is_in_late_show, is_with_family, is_student, discount_price):

4.ペア構成テスト(Pairwise法)

全てテストすることが現実的ではなく不可能な場合に利用されるテスト技法

  • 複数の入力に対して適用する
  • 例えば
    • あなたはグローバルに展開する決済サービスのテスターです
    • 世の中に存在する主要な端末(iPhoneSE/Pixel6a/RaspberyPI3 …)・全ての主要ブラウザ(Chrome/Firefox/Safari/Brave/Vivaldi …)全てで全世界のクレジットカードブランド(Visa/Mastercard/銀聯)を使って全世界の国々(日本/アメリカ/中国/イタリア …)から決済処理をテストする場合
    • 1回のテストにとても時間がかかるとする
      • 総当たりで全てテストできない、けどある程度テストしたい

4.ペア構成テスト(Pairwise法)

変数値の全てのペアをテストする

  • 総当たりではなく、2つの変数のペアが全通りテストされるようにテストケースを減らす
  • この手法の効果を実証する事例はいくつかある
    • NISTでは、医療機器のソフトウェア不具合の98%は、パラメータの全ペアをテストすることで気づけた
    • MozillaのWebブラウザの欠陥は76%がペア構成テストで気付けた

4.ペア構成テスト(Pairwise法)

自分でテストケースを作るのは大変…
一般的には2つ方法がある

  1. 直交表
    • 必要なテストケースは 1.変数の数, 2.各変数がとりえる数から決まるので、その値と同じまたは少し大きいプリセットの直交表を利用する
  2. 全ペアアルゴリズム
    • 全ペアをテストできるテストケースを生成するアルゴリズムやプログラムがあるので、それらを利用する

4.ペア構成テスト(Pairwise法) in Django

特に思いつかないですすいません🙏


まとめ

  • 色々なテストの方法があった
    • ブラックボックステストとホワイトボックステストに分けられる
    • 単一の入力の場合には、同値クラステスト・境界値テストが使える
    • 複数の入力の場合には、デシジョンテーブルテスト・ペア構成テストが使える
  • Djangoでテストを書く時に色々意識してみましょう
    • nullやkeyがない場合も忘れずにテストしましょう
    • 効果的ではないテストケースを選ばないよう気をつけましょう
    • @pytest.mark.parametrizeを使って、デシジョンテーブルをそのまま表現できるようにしよう

See also