代表値を固定する代わりに指定した範囲から確率的に取り出したサンプルを確かめる

目的

  • 未公開記事の発掘
  • 製品コードの特性をテストが知っているのは役割分担を間違えてるような気がする
  • 値域や事後条件をアノテーションで書けるといいのにな

メリットや使いどころ

パラメタライズドテスト

  • テスト条件と期待値の組を複数用意して、同じテストメソッドを繰り返し呼び出す手法です
  • 状態や入力値によって期待値の変化するロジックを効率的にチェックできます
  • JUnit5 では拡張ライブラリの junit-jupiter-params を利用します
  • JUnit4 では組み込みクラスの Parameterized テストランナーを利用します

ドメインの固有型

  • たとえば、業務日付を表現するのに組み込みの Date クラスではなく BusinessDate クラスを作成する設計手法です
  • コンパイル時や実行時にプロパティ(特性)を保護することができます
    • クラスの事前条件や事後条件に記述する
  • プログラミング言語レベルの設計の自由度が低下する場合があります
    • 固有型そのままでは一般的なライブラリの API が利用できないとか
  • 記述が冗長化する場合があります
    • 組み込み型との相互変換処理を提供するとか
    • システムAの業務日付とシステムBの業務日付を別々の固有型にするとか
      • 共通する部分を基底クラスに導出して派生クラスにするみたいなシステム都合の対応をするとドメインから乖離してしまう

具体例

サンプルプロジェクト を作ってみました。

  • /todo に REST エンドポイントを公開するだけの Spring Boot な Java アプリです
    • Controller > Service > Repository の 3 階層になっています
  • TodoServiceTest はプリミティブなユニットテストです
    • jqwik API で書き直したプロパティベーステストが TodoServicePropertyTest です

TodoServiceTest

TodoServiceTest には次のようなテストを記述しています。

  • 指定した ID で取得するデータが存在する場合のテスト
  • 指定した ID で取得するデータが存在しない場合のテスト
  • 永続化の成功を確認するテスト

複数のパラメータを使用するので、パラメタライズドテストのための @ParameterizedTest を使っています。

しかし、@ValueSource にあまり多くの値を記述すると読みにくくなるため、代表的な値しか記述していません。

@CsvFileSource を使うと、パラメーターをクラスパスに配置した CSV ファイルから読み取るようにできます。

TodoServicePropertyTest

TodoServicePropertyTest に記述しているテストは基本的に TodoServiceTest と同じです。

パラメタライズドテストを次のような API で置き換えています。

  • プロパティベーステスト @Property
    • 引数に指定した @ForAll は入力値を生成します
    • 生成した入力値から所定の回数だけ、ランダムに取り出してテストを実行します
  • 具体例ベーステスト @Example
    • プロパティベーステストと違って 1 度しか実行しません
    • API の使い方など、代表的な例を記述するのに向いていると思います
  • データ駆動テスト @FromData
    • @Data を指定したメソッドの返り値そのものを入力としてテストを実行します
    • テストメソッドの引数は 8 個まで対応していました

ユニットテストの目的

ユニットテストの目的は、プログラムの構成部品が設計したとおりに動作することを確かめることです。

  • 設計したとおりに動作する とは何?
    • 仕様 は図表や自然言語で記述する
    • つまり解釈による曖昧性がある
    • 仕様 をプログラムで実現可能な表現に翻訳したのが 設計
    • 設計 には暗黙的な前提条件や状態、手段を記述する
    • ユニットテストの対象は 仕様 ではなく 設計 なので、仕様 に記述されていない要素も確かめないといけない
  • 設計したとおりに動作することを確かめる とは何?
    • 設計 に記述された要素を実現できているか確かめること
      • (あるなら)前提条件を満たしているか確かめる
      • (変更しているなら)意図した状態に変更できているか確かめる
      • (あるなら)入力に対する出力が正しいか確かめる
      • (あるなら)事後条件を満たしているか確かめる

設計 に記述してあることだけを確かめるわけではないので注意が必要です。

  • 設計 には 仕様 から導出された正常系だけしか記述されていない場合が多い
    • 記述が不足している場合もある
    • そもそも記述できない場合もある
      • 主に量の都合
  • 設計 に記述されてないから確認しなくていい?
    • No !!!
    • 設計 をインプットにテストを設計する
  • テストを設計する とは何?
    • プログラムの構成部品を状態遷移機械に見立てて、入力と次状態を整理すること
    • 入力を構成する要素:前提条件、事前状態、入力
    • 次状態を構成する要素:事後条件、事後状態、出力

ユニットテストに可能な全ての入力を与えるのは大変なので工夫します。

  • 同値分割
    • 値の範囲を分割して代表値を選ぶ
  • 境界値分析
    • 分割した範囲の境目を選ぶ
    • 内、境界、外

しかし、工夫した結果の正しさを厳密に保証する手法は存在しないため、設計とは異なる観点のレビューや確認(元の要素からの過不足など)が必要になります。

プロパティベーステストの目的

  • プロパティベーステストは QuickCheck という Haskell の実装から成立した手法
  • ランダムな入力値生成による失敗パスの発見を目指すところがファジングと似ている

この辺の説明がシンプルで分かりやすいと思います。