编写与运行测试
SuperJ 有两种测试机制。本指南覆盖两者,但大多数 SDK 与应用测试使用第一种。
单元测试(sj.test) | Golden-output 测试 | |
|---|---|---|
| 位于 | test/ | examples/ |
| 你编写 | 带 Asserts.* 的 @Test 方法 | 一个会打印的程序,加上 expected_<name>.txt |
| 运行方式 | superj test(自托管) | superj golden examples(自托管) |
| 通过意味着 | 每个断言都成立(退出码 0) | stdout 与期望文件逐字节匹配 |
| 用于 | 库/逻辑检查 — map、解析器、数学、池 | SPEC 一致性、代码生成输出、"打印结果就是规格" |
经验法则:如果你会用 assertEquals,就写单元测试;如果你会 println 然后目测结果,就写 golden 测试。
用 sj.test 做单元测试
第一个测试
在 test/ 下创建一个以 Test.sj 结尾的文件,标记 // UNIT,并编写 @Test static void 方法。无需 main,无需注册 — 编译器自动接线:
// UNIT
import sj.util.IntArrayList;
import sj.test.Asserts;
class IntArrayListTest {
@Test
static void startsEmpty() {
IntArrayList list = new IntArrayList();
Asserts.assertTrue("new list is empty", list.isEmpty());
Asserts.assertEquals("size", 0, list.size());
}
@Test
static void addAndGet() {
IntArrayList list = new IntArrayList();
list.add(10);
list.add(20);
Asserts.assertEquals("size", 2, list.size());
Asserts.assertEquals("first", 10, list.get(0));
}
}
运行它:
superj test # 构建编译器 + SDK,然后运行每个 test/ 套件
输出:
test IntArrayListTest.startsEmpty ... ok
test IntArrayListTest.addAndGet ... ok
test result: ok. 2 passed; 0 failed; 0 ignored; 0 filtered out
规则
- 文件:位于
test/下,前五行内含// UNIT(superj test查找的标记)。每文件一个类;命名为<Thing>Test.sj。 @Test标记一个static void无参方法。编译器合成运行器main,把每个@Test注册为"ClassName.methodName",并以失败计数退出。(不带@Test的方法 — 辅助方法、capture()目标 — 永不作为测试运行。)它必须是static,因为没有反射(SuperJ 移除了它),运行器无法在运行时实例化类或调用实例方法 — 所以注册是一个编译期方法引用(ClassName::method,如下文显式main所示),而一个无接收者的无参方法引用没有实例可绑定。- 测试通过即返回,失败即抛出。一个
Asserts失败抛出AssertionFailure;任何其他Throwable(例如意外的 NPE)也算失败,而不是整个运行的崩溃。 - 如果你需要自定义入口点,仍可写一个显式
main(构建一个TestRunner并手动调用t.add("name", Class::method))— 显式main会抑制合成的那个。@Test是常见路径。
断言 — sj.test.Asserts
全部为 static,消息在前,失败时抛出 AssertionFailure(消息打印在 FAILED 行下方):
| 方法 | 备注 |
|---|---|
assertTrue(msg, cond) / assertTrue(cond) | |
assertFalse(msg, cond) / assertFalse(cond) | |
assertEquals(msg, expected, actual) | 有 int、long、boolean、double、String、Object(通过 .equals)的重载 |
assertEquals(msg, expected, actual, epsilon) | double 在容差范围内 — 用于计算出的浮点数 |
assertNotEquals(msg, unexpected, actual) | int、long |
assertNull(msg, value) / assertNotNull(msg, value) | |
fail(msg) | 无条件失败 |
capture(TestCase body) → Throwable | 运行 body,返回它抛出的内容(或 null)— 用于异常测试 |
测试某物会抛出
无反射:capture() 运行一个 body 并把它抛出的东西交给你,然后你用 instanceof 检查。body 是一个指向非 @Test 辅助方法的方法引用。
class PoolTest {
@Test
static void rejectsNull() {
Throwable t = Asserts.capture(PoolTest::offerNull);
Asserts.assertTrue("throws IAE", t instanceof IllegalArgumentException);
}
static void offerNull() { // 辅助方法 — 无 @Test
new ObjectPool<Box>(2, null).offer(null);
}
}
跳过测试 — @Ignore
加 @Ignore(与 @Test 一起)以搁置一个已知失败或不稳定的测试。它被报告为 ... ignored,从不运行,且绝不算作失败:
@Test
@Ignore
static void flakyUntilFixed() {
Asserts.assertEquals("known bad", 1, 2); // 被跳过 — 套件保持绿色
}
test SomeTest.flakyUntilFixed ... ignored
test result: ok. 4 passed; 0 failed; 1 ignored; 0 filtered out
@Ignore 不带 @Test 是编译错误。
A/B 差分测试
没有专门特性 — 在一个 @Test 中计算一个参考(oracle)结果与优化后/真实结果,然后在多个输入上 assertEquals 它们。示例:一个插入排序 oracle 与 SDK 的 Arrays.sort 在带种子的随机数组上对比(见 test/SortDifferentialTest.sj):
@Test
static void arraysSortMatchesReference() {
Random rng = new Random(12345L);
for (int trial = 0; trial < 500; trial = trial + 1) {
int n = rng.nextInt(64);
int[] data = new int[n];
for (int i = 0; i < n; i = i + 1) data[i] = rng.nextInt(1000) - 500;
int[] expected = referenceSort(data); // A: 平凡正确的 oracle
int[] actual = new int[n]; // B: 被测路径
System.arraycopy(data, 0, actual, 0, n);
Arrays.sort(actual);
for (int i = 0; i < n; i = i + 1)
Asserts.assertEquals("trial " + trial + " idx " + i, expected[i], actual[i]);
}
}
运行测试
superj test # 标准门禁:构建 + 运行全部 test/
./tools/superj test # 同上,若编译器 + SDK 已构建(目录默认为 test/)
./tools/superj test test --filter pool # 仅名称包含 "pool" 的用例
superj test:
- 发现目录(默认
test/)下每个// UNIT文件, - 对每个用
--sdk-source编译并链接, - 运行它,并按退出码汇总通过/失败 — 一个非零套件让
superj test退出非零(CI 就绪)。
选项:--filter <substr>(转发给每个套件;仅运行匹配的用例)、--sdk-source <dir>(默认 sdk/sj)、--clang-path <path>。
注意事项
- 单元测试不被 golden 套件运行 — 它们是独立的步骤。对 SDK 逻辑的改动应同时运行
superj golden examples与superj test。 - 编译器改动后重新构建 SDK(
superj compile-sdk),再运行任一套件,否则过时归档的链接错误看起来像测试失败。 - 套件用
--sdk-source编译,所以从另一个类读取的跨类static final int常量在较旧的编译器中可能表现为0(#529 家族);若遇到,优先用方法或同类常量。
Golden-output 测试
用于 SPEC 一致性与代码生成行为,打印输出就是断言。把程序放在 examples/<name>.sj,期望 stdout 放在 examples/expected_<name>.txt;superj golden examples 编译、运行并做 diff。
// examples/test_my_feature.sj
public class test_my_feature {
public static void main(String[] args) {
System.out.println("answer=" + (6 * 7));
}
}
# examples/expected_test_my_feature.txt
answer=42
superj golden examples # 自托管;运行所有类别:positive、negative、
# smoke、SDK positive/smoke/compile
其他 golden 约定(文件前五行):// ERROR: <substr> 标记一个必须编译失败并带该消息的负面测试;// WARNING: <substr> 标记一个必须警告的编译。不要把单元逻辑断言放进 golden 测试 — 每个新检查都强制重新生成 golden 文件,而第一个不匹配会掩盖其余。改用 sj.test 单元测试。