测试运行器接口


测试运行器只有一个职责:接收提交的解答,运行所有测试,并返回标准化的输出。 与 Exercism 网站的所有交互都会自动处理,不属于本规范的范围。

执行

  • 测试运行器应提供一个可执行脚本。更多信息请参见 docker.md 文件。
  • 该脚本会接收三个参数:
    • 练习的 slug(例如 two-fer)。
    • 输入目录的路径(末尾带斜杠),其中包含提交的解答文件以及练习的其他文件。该目录应视为只读。技术上可以写入其中,但临时文件(例如编译源码时)最好放在 /tmp。
    • 输出目录的路径(末尾带斜杠)。该目录可写。
  • 脚本必须把 results.json 文件写入输出目录。
  • 只要运行成功,无论测试状态如何,运行器都必须以退出码 0 退出。

允许的运行时间

针对每个提交的解答,测试运行器都有 20 秒的时间,期间可占用 100% 的 CPU 和 3GB 内存。 20 秒后,进程会被中止,并报告超时。

Note

我们强烈建议遵循性能最佳实践文档,以降低超时发生的可能性。

输出格式

results.json 文件支持以下字段:

顶层

版本

键:version,类型:number,是否必需:必需

版本:1、2、3

本文件所遵循的规范版本:

  • 1:适用于测试运行器无法提供单个测试信息的赛道。
  • 2:适用于测试运行器能够输出单个测试信息的赛道。含有概念练习的赛道至少需要此版本。
  • 3:适用于测试运行器能够把单个测试关联到某个任务的赛道。

状态

键:status,类型:string,是否必需:必需

版本:1、2、3

以下整体状态有效:

  • pass:所有测试通过
  • fail:至少有一个测试的状态为 fail 或 error
  • error:没有执行任何测试(这通常意味着编译错误或语法错误)

error 状态只应在所有测试都出错时使用。 对编译型语言来说,这通常是代码无法编译导致的。 对解释型语言来说,这是运行时错误,比如导致文件无法解析的语法错误。

消息

键:message,类型:string,是否必需:当 status = error 时必需,或当 status = fail 且 version = 1 时必需

版本:1、2、3

当状态为 error(没有正确执行任何测试)时,应提供顶层 message 键。它应向用户说明发生的错误。由于这是用户了解如何调试问题的唯一信息,它必须尽可能清晰:

  • 把路径简化成类似 <solution-dir>/relative/path 的形式,而不是 /full/path/to,因为后者会包含对用户无用的 ECR 特定数据
  • 在可能或适用时,合并掉非用户代码的调用栈
  • 绝不要显示没有上下文(即错误消息)的调用栈
  • 尽可能不要改动错误消息,这样便于搜索该错误

在 Ruby 中,遇到语法错误时,我们会提供运行时错误和堆栈跟踪。在编译型语言中,应提供编译错误。

顶层 message 的值限制为 65535 个字符。 如果值包含多字节字符,有效最大长度会更小。

当状态不是 error 时,要么把值设为 null,要么完全省略该键。

测试

键:tests,类型:array,是否必需:当 status = fail 或 status = pass 时必需

版本:2、3

这是一个测试结果数组,具体见下文“每个测试”一节。

测试必须按测试文件中指定的顺序返回。 对于以随机顺序执行测试的语言,这可能意味着要按照测试文件中指定的顺序重新排列结果。

这样做的理由是,只会向学生显示第一个失败,因此显示正确的失败非常重要。由于测试文件中的测试通常按 TDD 方式排序,而且在实践练习中学生会看到编辑器里的测试文件,所以让结果与测试文件保持一致至关重要。

每个测试

名称

键:name,类型:string,是否必需:必需

版本:2、3

这是测试的名称,采用人类可读的格式。

测试代码

键:test_code,类型:string,是否必需:当练习是概念练习时必需

版本: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"

(其中换行符会被替换为 \n 以保证 JSON 有效)。

状态

键:status,类型:string,是否必需:必需

版本:2、3

以下单个测试状态有效:

  • pass:测试通过
  • fail:测试失败
  • error:测试出错,即它没有返回值

消息

键:message,类型:string,是否必需:当 status 为 fail 或 error 时必需

版本:2、3

单个测试的 message 键用于返回 status 为 fail 或 error 的测试结果。它应尽可能人类可读。这里写的内容会在学生测试未通过时显示给他们。如果没有测试失败消息或错误消息,要么把值设为 null,要么完全省略该键。也允许在这里输出测试套件的输出。message 的值没有长度限制。

输出

键:output,类型:string,是否必需:可选

版本:2、3

单个测试的 output 键用于存储并输出用户为某项测试有意输出的任何内容。

  • 它应附加到所有会产生用户输出的测试结果上。
  • 只应显示用户手动输出的内容,而不是测试运行器自动输出的内容。
  • 你可以捕获通过常规方式输出的内容(例如 Ruby 的 puts、Python 的 print 或 C# 的 Debug.WriteLine),也可以提供一种供用户使用的方法(例如 Ruby 测试运行器为用户提供了一个全局可用的 debug 方法,其特性与标准的 puts 方法相同)。
  • 输出必须限制在 500 个字符以内。在这种情况下,无论是截断并附带“输出已被截断。请限制在 500 字符以内”的消息,还是返回错误,都是可以接受的。

任务 ID

键:task_id,类型:number,是否必需:可选

版本:3

通过任务的 ID 把测试关联到特定任务,该 ID 是任务标题开头使用的数字。只有当某个测试能精确关联到一个任务时,才把它关联到任务。

目前,只有概念练习有明确定义、可供关联测试的任务,但将来可能会改变。

例如,看下面这个 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

...

这些说明定义了两个任务:

  1. 以分钟为单位定义预期的烤箱时间
  2. 以分钟为单位计算剩余的烤箱时间

那么 results.json 文件可以有这样一个条目:

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

这个测试现在关联到了第一个任务:“以分钟为单位定义预期的烤箱时间”。注意,名称不必与任务的描述一致。

赛道可以有多种方式实现这一点:

  • 在测试文件内为测试添加元数据(例如使用特性/注解/注释),让测试运行器在运行测试时读取这些元数据。
  • 把测试名称与任务 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>

如何为你所用语言的测试套件添加元数据

条条大路通罗马,达成这一点并没有规定的模式。 到目前为止,已经采用过几种做法:

  • 手动编译辅助 JSON 文件,在测试运行时与测试结果合并。
  • 对测试套件进行自动化静态分析,在测试运行时与测试结果合并。
    • 这可以通过 AST 分析或文本解析来实现