テストジェネレーターとは、演習のテストを自動生成するための、トラック固有のソフトウェアです。演習のJSONテストケースを、そのトラックの言語のテストに変換することでこれを実現します。
テストジェネレーターを導入すると、次のようなメリットがあります。
一般に、テストジェネレーターを実行する目的は次の2つのいずれかです。
新しい演習にテストジェネレーターを追加すると、そのテストファイルを生成できるようになります。テストジェネレーター自体がすでに実装されていれば、新しい演習のテストを生成する作業は、一から書くよりも(はるかに)少ない手間で済みます。
演習にテストジェネレーターがあれば、それを再実行して、演習を最新のカノニカルデータに更新・同期できます。更新が必要な問題のあるテストケースがないか、あるいは追加したい新しいテストがないかを確認するために、定期的に行うことをおすすめします。
演習のテストジェネレーターを実装するときの出発点は、2つ考えられます。
既存のテストがある場合は、生成するテストが既存の解答を壊さないようにテストジェネレーターを実装してください。
大まかに言うと、テストファイルは次のいずれかの方法で生成します。
コードベースの方法ではテストジェネレーターのコードがかなり複雑になりがちですが、テンプレートベースの方法はよりシンプルです。
おすすめの流れは次のとおりです。
tests.tomlファイルでinclude = falseとマークされたテストケースを除外しますこの構成の大きなメリットは、各演習が独自のテンプレートを持つことです。これによって次のことが得られます。
テストジェネレーターを設計するときは、次の点を意識してください。
テストジェネレーターは通常、(ほとんど)トラックの言語で書きます。
他の言語を使うことも自由ですが、言語が増えるほどトラックのメンテナンスやコントリビュートが難しくなります。そのため、可能なかぎりトラックの言語を使うことをおすすめします。メンテナンスやコントリビュートがしやすくなるからです。
トラックにコードをフォーマットするツールがある場合は、テンプレートをレンダリングした_後に_、後処理のステップとして実行することを検討してください。
テストジェネレーターが扱う中心となるデータは、演習のcanonical-data.jsonファイルです。このファイルはexercism/problem-specificationsリポジトリーで定義されており、そこではExercismの多くの演習の共有メタデータが定義されています。
すべての演習に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配列の各要素は次のいずれかです。
要素の種類は、その種類だけが持つフィールドの有無を確認することで判別できます。おそらく最も簡単なのは、テストケースのグループにのみ存在する"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
}
}
]
}
]
}
トラックがテストのグループ化に対応していない場合は、次のことを行う必要があります。
cases階層をたどって平坦化し、最も内側の(葉の)テストケースだけを取り出すinputとexpectedというテストケースのキーの中身は、実にさまざまです。ほとんどの場合、スカラー値(数値、真偽値、文字列など)か単純なオブジェクトです。しかしときには、もう少し複雑な値も出てきます。疑似コードのラムダや、学習者のコードに対して実行する操作のリストなどで、これらには多少の前処理が必要になることが多いでしょう。
テストケースには任意のscenariosフィールドがあります。このフィールドを使うと、テストジェネレーターで特定のテストケースを特別扱いできます。最も一般的なのは、特定の種類のテストを無視する場合です。たとえば、トラックの言語がUnicodeに対応していない可能性がある場合は、"unicode"シナリオのテストを無視します。
シナリオの全リストはこちらにあります。
canonical-data.jsonファイルを読み込む方法は、いくつかあります。
problem-specificationsリポジトリーから直接取得します(例: https://raw.githubusercontent.com/exercism/problem-specifications/main/exercises/leap/canonical-data.json)。problem-specificationsリポジトリーをGitサブモジュールとしてトラックのリポジトリーに追加します。configletのキャッシュから読み込みます。場所はユーザーのシステムによって異なりますが、configlet info -o -v d | head -1 | cut -d " " -f 5を使えば、プログラムから場所を取得できます。トラックに固有の追加テストケース(カノニカルデータには存在しないもの)を加えたい場合の一つの方法は、additional-test-cases.jsonファイルを作ることです。テストジェネレーターはこれをcanonical-data.jsonファイルとマージしてから、レンダリングのためにテンプレートへ渡します。
使うテンプレートエンジンは、おそらくトラック固有のものになります。理想的には、テンプレートはできるだけ素直なものにしたいところです。コードの重複などは気にしなくてかまいません。
テンプレート自体はテストジェネレーターからデータを受け取ります。テストジェネレーターはそのデータを繰り返し処理して、テンプレートをレンダリングします。
テンプレートをシンプルに保つには、テストジェネレーター側で少し前処理を行うか、あるいはテンプレートが対応している拡張の仕組み(「フィルター」など)を定義するとよいでしょう。
configletはトラックの主要なメンテナンスツールで、次のことに使えます。
bin/configlet create --practice-exercise <slug>を実行しますtests.tomlファイルを同期する: bin/configlet sync --tests --update --exercise <slug>を実行しますそのためconfigletは、テストジェネレーターと組み合わせることで非常に強力なワークフローを実現できる優れたツールです。
テストジェネレーターは、_簡単_かつ強力に使えるようにしたいものです。そのためには、1つ以上のスクリプトファイルを作ることをおすすめします。
トラックに最適なスクリプトファイルの形式は自由に選んでかまいません。シェルスクリプトとPowerShellスクリプトはよく使われる選択肢で、どちらも問題なく機能します。
次は、configletとテストジェネレーターを組み合わせて、新しい演習をすばやくひな形として生成するシェルスクリプトの例です。
bin/fetch-configlet
bin/configlet create --practice-exercise <slug>
path/to/test-generator <slug>
テストジェネレーターの作成を始める前に、既存のテストジェネレーターをいくつか見て、他のトラックがどのように実装しているかを感じ取ることをおすすめします。
質問がある場合は、フォーラムで尋ねるのが一番です。RustとJavaScriptのテストジェネレーターに関するフォーラムでの議論も、参考になるでしょう。
テストジェネレーターは、実用最小限の製品から始めて、段階的に作っていくことをおすすめします。最小限のバージョンは、演習のcanonical-data.jsonを読み込み、そのデータをただテンプレートに渡すだけです。
まずは1つの演習だけに集中しましょう。leapのようなシンプルなものがおすすめです。それが動くようになってから、少しずつ演習を増やしていきます。
そして、テストジェネレーターはできるだけシンプルに保ちましょう。
理想的には、コントリビューターがテストジェネレーターの内部動作を理解しなくても、既存のテンプレートを貼り付けて少し変えるだけで済むのがよいでしょう。
テストジェネレーターの使い方やコントリビュート方法は、トラックごとに異なります。トラックのREADME.md、CONTRIBUTING.md、またはテストジェネレーターのコードがあるディレクトリの説明を参照してください。