テストランナーのインターフェース


テストランナーの役割はただ1つ、解答を受け取り、すべてのテストを実行し、標準化された出力を返すことです。 Exercismのウェブサイトとのやり取りはすべて自動で行われるため、この仕様には含まれません。

実行

  • テストランナーは、実行可能なスクリプトを提供する必要があります。詳しくはdocker.mdファイルを参照してください。
  • スクリプトは3つのパラメータを受け取ります。
    • 演習のスラッグ(例:two-fer)。
    • 提出された解答ファイルとその他の演習ファイルを含む入力ディレクトリへのパス(末尾にスラッシュが付きます)。このディレクトリは読み取り専用とみなしてください。技術的には書き込むこともできますが、一時ファイル(ソースのコンパイルなど)には/tmpを使うのがよいでしょう。
    • 出力ディレクトリへのパス(末尾にスラッシュが付きます)。このディレクトリには書き込むことができます。
  • スクリプトは、出力ディレクトリにresults.jsonファイルを書き出さなければなりません。
  • ランナーは、テストの結果にかかわらず、正常に実行を終えた場合は終了コード0で終了しなければなりません。

許容される実行時間

テストランナーには、解答1つあたり20秒間、CPU100%とメモリ3GBが割り当てられます。 20秒を過ぎると、プロセスは停止され、タイムアウトとして報告されます。

Note

タイムアウトの可能性を減らすために、パフォーマンスのベストプラクティスに従うことを強くおすすめします。

出力フォーマット

results.jsonファイルでは、以下のフィールドを使用できます。

トップレベル

バージョン

key: version、type: number、presence: 必須

version: 1, 2, 3

このファイルが準拠する仕様のバージョンです。

  • 1:テストランナーが個々のテストの情報を提供できないトラック向けです。
  • 2:テストランナーが個々のテストの情報を出力できるトラック向けです。コンセプト演習があるトラックでは、これが最低限必要なバージョンです。
  • 3:テストランナーが個々のテストをタスクに紐付けられるトラック向けです。

ステータス

key: status、type: string、presence: 必須

version: 1, 2, 3

全体ステータスとして有効なのは、次のとおりです。

  • pass:すべてのテストが通りました
  • fail:少なくとも1つのテストのステータスがfailまたはerrorです
  • error:テストが1つも実行されませんでした(多くの場合、コンパイルエラーか構文エラーを意味します)

errorステータスは、すべてのテストがエラーになった場合に_のみ_使ってください。 コンパイル言語では、通常これはコードがコンパイルできなかった結果です。 インタプリター言語では、これは実行時エラーです。たとえば、ファイルの解析を止めてしまう構文エラーなどです。

メッセージ

key: message、type: string、presence: status = errorの場合、またはstatus = failかつversion = 1の場合は必須

version: 1, 2, 3

ステータスがerror(テストが1つも正しく実行されなかった)の場合、トップレベルのmessageキーを指定してください。ここには、発生したエラーをユーザーに伝える内容を書きます。ユーザーが問題をデバッグするために受け取る唯一の情報なので、できるだけ明確に書かなければなりません。

  • パスは/full/path/toのような形式ではなく、<solution-dir>/relative/pathのように簡略化してください。/full/path/toには、役に立たないECR固有の情報が含まれてしまうからです
  • 可能な場合は、ユーザーのコード以外のスタックを折りたたんでください
  • 文脈(つまりエラーメッセージ)のないコールスタックは決して表示しないでください
  • (可能であれば)エラーメッセージは変更しないでください。そのほうがエラーを検索しやすくなります

Rubyの場合、構文エラーのときは、実行時エラーとスタックトレースを提供します。コンパイル言語では、コンパイルエラーを提供してください。

トップレベルのmessageの値は、65535文字に制限されています。 マルチバイト文字が含まれる場合、実際の最大長はそれより短くなります。

ステータスがerrorでない場合は、値をnullにするか、キー自体を省略してください。

テスト

key: tests、type: array、presence: status = failまたはstatus = passの場合は必須

version: 2, 3

これはテスト結果の配列で、後述の「テストごと」セクションで説明します。

テストは、テストファイルに指定された順序で返さなければなりません。 テストをランダムな順序で実行する言語では、テストファイルに指定された順序に合わせて結果を並べ替える必要があるかもしれません。

その理由は、学習者には最初の失敗だけが表示されるため、正しい失敗が表示されることが重要だからです。テストは通常、テストファイル内でTDDの流れに沿って並んでおり、プラクティス演習では学習者がエディターでテストファイルを見ることができるため、結果をテストファイルと一致させることが重要です。

テストごと

名前

key: name、type: string、presence: 必須

version: 2, 3

これは、人間が読める形式でのテストの名前です。

テストコード

key: test_code、type: string、presence: 演習がコンセプト演習の場合は必須

version: 2, 3

これは、コンセプト演習では必ず含めなければなりません。プラクティス演習では含めるべきです。 この要件の違いは、コンセプト演習では学習者にテストが表示されないため、test_codeが表示されないと演習を解くのが難しくなる一方、プラクティス演習ではテストが表示されることに由来します。

これは、テスト対象となるコマンドの本体です。たとえば、次のRubyのテストは、

def test_duplicate_items_uniqs_list
  cart = ShoppingCart.new
  cart.add(:STARIC)
  cart.add(:MEDNEW)
  cart.add(:MEDNEW)
  assert_equal 'Newspaper, Rice', cart.items_list
end

次のようなtest_codeの値を返すべきです。

"cart = ShoppingCart.new
cart.add(:STARIC)
cart.add(:MEDNEW)
cart.add(:MEDNEW)
assert_equal 'Newspaper, Rice', cart.items_list"

(JSONとして有効にするため、改行は\nに置き換えます)。

ステータス

key: status、type: string、presence: 必須

version: 2, 3

テストごとのステータスとして有効なのは、次のとおりです。

  • pass:テストが通りました
  • fail:テストが失敗しました
  • error:テストがエラーになりました。つまり、値が返されませんでした

メッセージ

key: message、type: string、presence: statusがfailまたはerrorの場合は必須

version: 2, 3

テストごとのmessageキーは、statusがfailまたはerrorのテストの結果を返すために使います。できるだけ人間が読みやすい形にしてください。ここに書いた内容は、学習者のテストが通らなかったときに表示されます。テストの失敗メッセージやエラーメッセージがない場合は、値をnullにするか、キー自体を省略してください。ここにテストスイートの出力を書いてもかまいません。messageの値には長さの制限がありません。

出力

key: output、type: string、presence: 任意

version: 2, 3

テストごとのoutputキーは、ユーザーがテストのために意図的に出力したものを保存して表示するために使います。

  • ユーザーの出力を生み出すすべてのテスト結果に付けてください。
  • 表示されるのは、ユーザーが手動で出力した内容だけです。テストランナーによる自動的な出力は表示しません。
  • 通常の手段で出力される内容を取得してもよいですし(例:Rubyのputs、Pythonのprint、C#のDebug.WriteLine)、ユーザーが使えるメソッドを用意してもかまいません(例:Rubyのテストランナーは、標準のputsメソッドと同じ特性を持つ、グローバルに使えるdebugメソッドをユーザーに提供しています)。
  • 出力は500文字に制限しなければなりません。この場合は、「Output was truncated. Please limit to 500 chars」というメッセージとともに切り詰めても、エラーを返してもかまいません。

タスクID

key: task_id、type: number、presence: 任意

version: 3

テストを、タスクのIDを介して特定のタスクに紐付けます。このIDは、タスクの見出しの先頭で使われている番号です。テストは、正確に_1つ_のタスクに紐付けられる場合にのみ、タスクにリンクしてください。

現時点では、テストを紐付けられる明確なタスクがあるのはコンセプト演習だけですが、今後変わるかもしれません。

たとえば、次のinstructions.mdファイルを考えてみましょう。

# Instructions

You're going to write some code to help Lucian cook an exquisite lasagna from his favorite cook book.

## 1. Define the expected oven time in minutes

...

## 2. Calculate the remaining oven time in minutes

...

この指示では、2つのタスクが定義されています。

  1. オーブンの所要時間を分単位で定義する
  2. 残りのオーブン時間を分単位で計算する

その場合、results.jsonファイルには次のようなエントリを書けます。

{
  "name": "Expected oven time in minutes",
  "status": "pass",
  "task_id": 1,
  "test_code": "Assert.Equal(40, Lasagna.ExpectedMinutesInOven());"
}

これで、このテストは最初のタスク「オーブンの所要時間を分単位で定義する」に紐付きました。nameがタスクの説明と一致している必要は_ありません_。

トラックでこれを実装する方法はいくつかあります。

  • テストファイル内のテストにメタデータを追加し(属性・アノテーション・コメントなどを使います)、テストランナーがテスト実行時にこのメタデータを読み取る。
  • テスト名とタスクIDの対応を別のファイル(演習の.meta/config.jsonファイルなど)に保存し、この情報を生成されたresults.jsonファイルにマージする。

例

以下は、バージョンごとに有効なresults.jsonファイルがどんな形になるかの例です。

v1の例

{
  "version": 1,
  "status": "fail",
  "message": "Failed: test_answer\nExpected: 42, actual: 3"
}

v2の例

{
  "version": 2,
  "status": "fail",
  "message": null,
  "tests": [
    {
      "name": "Test that the thing works",
      "status": "fail",
      "message": "Expected 42 but got 123123",
      "output": "Debugging information output by the user",
      "test_code": "assert_equal 42, answerToTheUltimateQuestion()"
    }
  ]
}

v3の例

{
  "version": 3,
  "status": "fail",
  "message": null,
  "tests": [
    {
      "name": "Test that the thing works",
      "status": "fail",
      "message": "Expected 42 but got 123123",
      "output": "Debugging information output by the user",
      "test_code": "assert_equal 42, answerToTheUltimateQuestion()",
      "task_id": 1
    }
  ]
}

UI/UXに関する考慮事項

テストが失敗したとき

学習者の解答がテストに失敗したときは、次のような表示になるべきです。

Test Code:
  <test_code>

Test Result:
  <message>

テストが成功したとき

解答がテストに通ったときは、次のような表示になるべきです。

Test Code:
  <test_code>

言語のテストスイートにメタデータを追加する方法

やり方は1つではなく、決まったパターンがあるわけでもありません。 これまでに採られてきた方法はいくつかあります。

  • 補助的なJSONファイルを手作業で作り、テスト実行時にテスト結果とマージする。
  • テストスイートを自動で静的解析し、テスト実行時にテスト結果とマージする。
    • これは、AST解析やテキストのパースで実現できるかもしれません。