テストジェネレーター


テストジェネレーターとは、演習のテストを自動生成するための、トラック固有のソフトウェアです。演習のJSONテストケースを、そのトラックの言語のテストに変換することでこれを実現します。

メリット

テストジェネレーターを導入すると、次のようなメリットがあります。

  1. 演習をより速く追加できます
  2. 演習を追加するときの「退屈な」部分を自動化できます
  3. テストを最新のカノニカルデータと簡単に同期できます

ユースケース

一般に、テストジェネレーターを実行する目的は次の2つのいずれかです。

  1. _新しい_演習のテストを生成すること
  2. _既存の_演習のテストを更新すること

新しい演習のテストを生成する

新しい演習にテストジェネレーターを追加すると、そのテストファイルを生成できるようになります。テストジェネレーター自体がすでに実装されていれば、新しい演習のテストを生成する作業は、一から書くよりも(はるかに)少ない手間で済みます。

既存の演習のテストを更新する

演習にテストジェネレーターがあれば、それを再実行して、演習を最新のカノニカルデータに更新・同期できます。更新が必要な問題のあるテストケースがないか、あるいは追加したい新しいテストがないかを確認するために、定期的に行うことをおすすめします。

出発点

演習のテストジェネレーターを実装するときの出発点は、2つ考えられます。

  1. 演習が新しく、テストがまだ存在しない場合
  2. 演習がすでに存在し、既存のテストがある場合
Caution

既存のテストがある場合は、生成するテストが既存の解答を壊さないようにテストジェネレーターを実装してください。

設計

大まかに言うと、テストファイルは次のいずれかの方法で生成します。

  • コード: テストファイルを(ほとんど)コードで生成します
  • テンプレート: テストファイルを(ほとんど)テンプレートで生成します

コードベースの方法ではテストジェネレーターのコードがかなり複雑になりがちですが、テンプレートベースの方法はよりシンプルです。

おすすめの流れは次のとおりです。

  1. 演習のカノニカルデータを読み込みます
  2. 演習のtests.tomlファイルでinclude = falseとマークされたテストケースを除外します
  3. 演習のカノニカルデータを、テンプレートで使える形式に変換します
  4. 演習のカノニカルデータを演習固有のテンプレートに渡します

この構成の大きなメリットは、各演習が独自のテンプレートを持つことです。これによって次のことが得られます。

  • テストファイルがどのように生成されるかが明確になります
  • デバッグがしやすくなります
  • 他の演習を壊すリスクなしに安全に編集できます
Caution

テストジェネレーターを設計するときは、次の点を意識してください。

  • テストジェネレーター内部でのカノニカルデータの前処理を最小限にする
  • テンプレート間の結合を減らす

実装

テストジェネレーターは通常、(ほとんど)トラックの言語で書きます。

Caution

他の言語を使うことも自由ですが、言語が増えるほどトラックのメンテナンスやコントリビュートが難しくなります。そのため、可能なかぎりトラックの言語を使うことをおすすめします。メンテナンスやコントリビュートがしやすくなるからです。

フォーマット

トラックにコードをフォーマットするツールがある場合は、テンプレートをレンダリングした_後に_、後処理のステップとして実行することを検討してください。

カノニカルデータ

テストジェネレーターが扱う中心となるデータは、演習のcanonical-data.jsonファイルです。このファイルはexercism/problem-specificationsリポジトリーで定義されており、そこではExercismの多くの演習の共有メタデータが定義されています。

Caution

すべての演習にcanonical-data.jsonファイルがあるわけではありません! ない場合は、テストジェネレーターが扱えるデータが存在しないため、テストを手動で作成する必要があります。

構造

カノニカルデータはJSONオブジェクトで定義されます。このオブジェクトには"cases"フィールドがあり、テストケースが格納されています。これらのテストケースは(通常)トラックのテストと1対1で対応します。

各テストケースにはいくつかのプロパティがあり、最も重要なのは説明、プロパティ、入力値、期待値です。次はleap演習のcanonical-data.jsonファイルの(一部の)例です。

{
  "exercise": "leap",
  "cases": [
    {
      "uuid": "6466b30d-519c-438e-935d-388224ab5223",
      "description": "year not divisible by 4 in common year",
      "property": "leapYear",
      "input": {
        "year": 2015
      },
      "expected": false
    },
    {
      "uuid": "4fe9b84c-8e65-489e-970b-856d60b8b78e",
      "description": "year divisible by 4, not divisible by 100 in leap year",
      "property": "leapYear",
      "input": {
        "year": 1996
      },
      "expected": true
    }
  ]
}

テストジェネレーターの主な役割は、このJSONデータをトラック固有のテストに変換することです。上のJSONをNimのテストコードに変換すると、次のようになります。

import unittest
import leap

suite "Leap":
  test "year not divisible by 4 in common year":
    check isLeapYear(2015) == false

  test "year divisible by 4, not divisible by 100 in leap year":
    check isLeapYear(1996) == true

canonical-data.jsonファイルの構造は詳しく文書化されており、JSONスキーマの定義もあります。

入れ子

一部の演習では、カノニカルデータで入れ子を使っています。つまり、cases配列の各要素は次のいずれかです。

  1. 通常のテストケース(子テストケースがないもの)
  2. テストケースのグループ(1つ以上の子テストケース)
Note

要素の種類は、その種類だけが持つフィールドの有無を確認することで判別できます。おそらく最も簡単なのは、テストケースのグループにのみ存在する"cases"キーを使う方法です。

次は、入れ子になったテストケースの例です。

{
  "cases": [
    {
      "uuid": "e9c93a78-c536-4750-a336-94583d23fafa",
      "description": "data is retained",
      "property": "data",
      "input": {
        "treeData": ["4"]
      },
      "expected": {
        "data": "4",
        "left": null,
        "right": null
      }
    },
    {
      "description": "insert data at proper node",
      "cases": [
        {
          "uuid": "7a95c9e8-69f6-476a-b0c4-4170cb3f7c91",
          "description": "smaller number at left node",
          "property": "data",
          "input": {
            "treeData": ["4", "2"]
          },
          "expected": {
            "data": "4",
            "left": {
              "data": "2",
              "left": null,
              "right": null
            },
            "right": null
          }
        }
      ]
    }
  ]
}
Caution

トラックがテストのグループ化に対応していない場合は、次のことを行う必要があります。

  • cases階層をたどって平坦化し、最も内側の(葉の)テストケースだけを取り出す
  • テストケースの説明を親の説明と組み合わせて、一意なテスト名を作る

入力と期待値

inputとexpectedというテストケースのキーの中身は、実にさまざまです。ほとんどの場合、スカラー値(数値、真偽値、文字列など)か単純なオブジェクトです。しかしときには、もう少し複雑な値も出てきます。疑似コードのラムダや、学習者のコードに対して実行する操作のリストなどで、これらには多少の前処理が必要になることが多いでしょう。

シナリオ

テストケースには任意のscenariosフィールドがあります。このフィールドを使うと、テストジェネレーターで特定のテストケースを特別扱いできます。最も一般的なのは、特定の種類のテストを無視する場合です。たとえば、トラックの言語がUnicodeに対応していない可能性がある場合は、"unicode"シナリオのテストを無視します。

シナリオの全リストはこちらにあります。

canonical-data.jsonファイルを読み込む

canonical-data.jsonファイルを読み込む方法は、いくつかあります。

  1. problem-specificationsリポジトリーから直接取得します(例: https://raw.githubusercontent.com/exercism/problem-specifications/main/exercises/leap/canonical-data.json)。
  2. problem-specificationsリポジトリーをGitサブモジュールとしてトラックのリポジトリーに追加します。
  3. configletのキャッシュから読み込みます。場所はユーザーのシステムによって異なりますが、configlet info -o -v d | head -1 | cut -d " " -f 5を使えば、プログラムから場所を取得できます。

トラック固有のテストケース

トラックに固有の追加テストケース(カノニカルデータには存在しないもの)を加えたい場合の一つの方法は、additional-test-cases.jsonファイルを作ることです。テストジェネレーターはこれをcanonical-data.jsonファイルとマージしてから、レンダリングのためにテンプレートへ渡します。

テンプレート

使うテンプレートエンジンは、おそらくトラック固有のものになります。理想的には、テンプレートはできるだけ素直なものにしたいところです。コードの重複などは気にしなくてかまいません。

テンプレート自体はテストジェネレーターからデータを受け取ります。テストジェネレーターはそのデータを繰り返し処理して、テンプレートをレンダリングします。

Note

テンプレートをシンプルに保つには、テストジェネレーター側で少し前処理を行うか、あるいはテンプレートが対応している拡張の仕組み(「フィルター」など)を定義するとよいでしょう。

configletを使う

configletはトラックの主要なメンテナンスツールで、次のことに使えます。

  • 新しい演習のファイル一式を作成する: bin/configlet create --practice-exercise <slug>を実行します
  • 既存の演習のtests.tomlファイルを同期する: bin/configlet sync --tests --update --exercise <slug>を実行します
  • 演習のカノニカルデータをディスクに取得する(これは上記いずれかのコマンドの副作用です)

そのためconfigletは、テストジェネレーターと組み合わせることで非常に強力なワークフローを実現できる優れたツールです。

コマンドラインインターフェース

テストジェネレーターは、_簡単_かつ強力に使えるようにしたいものです。そのためには、1つ以上のスクリプトファイルを作ることをおすすめします。

Note

トラックに最適なスクリプトファイルの形式は自由に選んでかまいません。シェルスクリプトとPowerShellスクリプトはよく使われる選択肢で、どちらも問題なく機能します。

次は、configletとテストジェネレーターを組み合わせて、新しい演習をすばやくひな形として生成するシェルスクリプトの例です。

bin/fetch-configlet
bin/configlet create --practice-exercise <slug>
path/to/test-generator <slug>

ゼロから作る

テストジェネレーターの作成を始める前に、既存のテストジェネレーターをいくつか見て、他のトラックがどのように実装しているかを感じ取ることをおすすめします。

質問がある場合は、フォーラムで尋ねるのが一番です。RustとJavaScriptのテストジェネレーターに関するフォーラムでの議論も、参考になるでしょう。

実用最小限の製品

テストジェネレーターは、実用最小限の製品から始めて、段階的に作っていくことをおすすめします。最小限のバージョンは、演習のcanonical-data.jsonを読み込み、そのデータをただテンプレートに渡すだけです。

まずは1つの演習だけに集中しましょう。leapのようなシンプルなものがおすすめです。それが動くようになってから、少しずつ演習を増やしていきます。

そして、テストジェネレーターはできるだけシンプルに保ちましょう。

Note

理想的には、コントリビューターがテストジェネレーターの内部動作を理解しなくても、既存のテンプレートを貼り付けて少し変えるだけで済むのがよいでしょう。

利用する場合とコントリビュートする場合

テストジェネレーターの使い方やコントリビュート方法は、トラックごとに異なります。トラックのREADME.md、CONTRIBUTING.md、またはテストジェネレーターのコードがあるディレクトリの説明を参照してください。