推荐一个好用的测试工具,值得体验!

221 天前
 moonbitlang

在软件开发领域中,测试是确保质量与可靠性的必要环节。俗话说得好“工欲善其事,必先利其器”,测试工具越简单、用户友好度越高,开发者编写测试的意愿度就越高。

为了满足大家的测试需求,MoonBit 标准库最近引入了 inspect 函数,我们也称之为 expect 测试,它可以帮助程序员快速编写测试。

相比于 OCaml 和 Rust ,MoonBit 提供了更加简洁高效的测试体验。我们的测试工具不仅操作简便,而且无需任何外部依赖,支持开箱即用,简化了测试流程。

这里简单介绍一下 MoonBit

MoonBit 是国内首个工业级编程语言及其配套工具链( https://www.moonbitlang.cn/) 是由粤港澳大湾区数字经济研究院(福田)基础软件中心打造的 AI 原生的编程语言以及开发者平台。MoonBit 自 2022 年 10 月推出以来,通过创新框架在程序语言界形成后发优势,已在编译速度、运行速度和程序体积上取得了显著的优势。MoonBit 平台的出现不仅仅作为一种编程语言,更提供一个完整的开发工具链,包括 IDE 、编译器、构建系统、包管理器等。

接下来,让我们进一步了解 inspect 函数的使用。

忽略掉与位置相关的参数后,inspect 函数签名为:

pub fn inspect(obj : Show, ~content: String = "")

这里 obj 是任意一个实现 Show 接口的对象,~content 是一个可选参数,表示我们所期望的 obj 转化为字符串后的内容。听起来有点绕?让我们先来看看 inspect 的基本用法:

01 基本用法

首先,让我们使用 moon new hello 创建一个新项目

此时项目的目录结构如下:

.
├── README.md
├── lib
│   ├── hello.mbt
│   ├── hello_test.mbt
│   └── moon.pkg.json
├── main
│   ├── main.mbt
│   └── moon.pkg.json
├── moon.mod.json

打开 lib/hello_test.mbt ,将内容替换为:

fn matrix(c: Char, n: Int) -> String {
  let mut m = ""
  for i = 0; i < n; i = i + 1 {
    for j = 0; j < n; j = j + 1 {
      m = m + c.to_string()
    }
    m += "\n"
  }
  m
}

这里,matrix 函数接受一个字符 c 和整数 n 作为参数,生成一个 n * n 大小的字符矩阵。

接下来,添加如下内容:

test {
  inspect(matrix('🤣', 3))?
}

打开终端,执行 moon test 命令可以观察到类似如下输出:

Diff:
----
🤣🤣🤣
🤣🤣🤣
🤣🤣🤣

----

这里的输出展示了 matrix 函数的实际输出和 ~content 参数的差异,执行 moon test -u 或者 moon test --update 可以观察到 lib/hello_test.mbt 文件中的测试块被自动更新成:

test {
  inspect(matrix('🤣', 3), ~content=
    #|🤣🤣🤣
    #|🤣🤣🤣
    #|🤣🤣🤣
    #|
  )?
}

让我们再把 n 改成 4 ,然后执行 moon test -u 可以观察到测试块被自动更新成:

test {
  inspect(matrix('🤣', 4), ~content=
    #|🤣🤣🤣🤣
    #|🤣🤣🤣🤣
    #|🤣🤣🤣🤣
    #|🤣🤣🤣🤣
    #|
  )?
}

02 稍微复杂的例子

一般来说,写完一个函数后,我们需要为其编写一些单元测试。最简单的是断言测试,MoonBit 标准库中提供了 @assertion.assert_eq 函数,用于判断两个值是否相等。对于上述例子来说测试编写相对容易,因为我们可以轻易预测输出结果是什么。

接下来让我们看一个稍微复杂一点的例子:如何测试计算斐波那契数列第 n 项的函数。

首先在 lib 目录下新建一个 fib.mbt 的文件,然后粘贴如下内容:

fn fib(n : Int) -> Int {
  match n {
    0 => 0
    1 => 1
    _ => fib(n - 1) + fib(n - 2)
  }
}

我们需要一些测试来验证我们的实现是否正确。如果用断言测试,那么编写测试的流程是什么呢?假设我们想测试输入为 10 时 fib 的结果,我们会写下类似如下的代码:

test {
  @assertion.assert_eq(fib(10), ???)?
}

当我们写下这个测试的时候,会立即遇到一个问题,我们并不知道 assert_eq 中右侧的期望值应该是什么。我们可以自己在纸上计算,或者找一份斐波那契数列参考列表、或者执行我们自己实现的 fib 函数。总之,不论用什么方法,我们需要得到 fib(10) 的期望值是 55 ,才能完成一个测试用例的编写。

此时 lib/fib.mbt 文件内容应为:

fn fib(n : Int) -> Int {
  match n {
    0 => 0
    1 => 1
    _ => fib(n - 1) + fib(n - 2)
  }
}

test {
  @assertion.assert_eq(fib(10), 55)?
}

然后执行 moon test 可以观察到如下输出:

Total tests: 2, passed: 2, failed: 0.

大家可以感受到,这里的反馈周期是很长的。通常来说,用这种方法编写测试并不是很愉悦。

对于 fib 这个例子我们可以相对容易找到一个正确的值作为参考。然而,大多数情况下,我们想测试的函数并没有其他“真值表”作为参考。我们需要做的是给这个函数提供输入,然后观察其输出是否符合我们的期望。这种模式比较常见,但其他工具链很少提供支持。因此,我们的 MoonBit 工具链提供了对这一测试模式的一等支持。通过使用 inspect 函数,我们只需要提供输入,而不需要提供期望值。

接下来,让我们用 inspect 函数编写输入分别为 8 ,9 ,10 时的测试用例,此时 lib/fib.mbt 文件的内容如下:

fn fib(n : Int) -> Int {
  match n {
    0 => 0
    1 => 1
    _ => fib(n - 1) + fib(n - 2)
  }
}

test {
  @assertion.assert_eq(fib(10), 55)?
}

test {
  inspect(fib(8))?
}

test {
  inspect(fib(9))?
}

test {
  inspect(fib(10))?
}

通过执行:

moon test

可以观察到实际输出与 inspect 函数中 ~content 的差异:

$ moon test
test username/hello/lib/fib.mbt::0 failed
expect test failed at path/to/lib/fib.mbt:10:3-10:18
Diff:
----
21
----

test username/hello/lib/fib.mbt::1 failed
expect test failed at path/to/lib/fib.mbt:14:3-14:18
Diff:
----
34
----

test username/hello/lib/fib.mbt::2 failed
expect test failed at path/to/lib/fib.mbt:18:3-18:19
Diff:
----
55
----

接下来,我们的工作变成了检查这个输出是否正确,如果确信这些输出是正确的,通过执行 moon test -u ,在 lib/fib.mbt 文件中对应的测试块会被自动更新成:

test {
  inspect(fib(8), ~content="21")?
}

test {
  inspect(fib(9), ~content="34")?
}

test {
  inspect(fib(10), ~content="55")?
}

通过这种编写测试然后立即获取反馈的模式,能够极大提升编写测试的愉悦感。

接下来,让我们来看一个修改函数行为导致输出变化的例子。

例如,我们现在想让 fib 的第一项从 1 而不是从 0 开始,首先我们将 fib 函数中的 0 => 0 修改为 0 => 1:

fn fib(n : Int) -> Int {
  match n {

然后执行 moon test 可以看到 expect test 自动为我们展示了前后差异:

$ moon test
test username/hello/lib/fib.mbt::0 failed: FAILED:path/to/lib/fib.mbt:10:3-10:36 `89 == 55`
test username/hello/lib/fib.mbt::1 failed
expect test failed at path/to/lib/fib.mbt:14:3-14:33
Diff:
----
2134
----

test username/hello/lib/fib.mbt::2 failed
expect test failed at path/to/lib/fib.mbt:18:3-18:33
Diff:
----
3455
----

test username/hello/lib/fib.mbt::3 failed
expect test failed at path/to/lib/fib.mbt:22:3-22:34
Diff:
----
5589
----

Total tests: 5, passed: 1, failed: 4.

这里的输出结果发生了位移,符合期望,我们大概率可以确定输出是对的。于是通过执行 moon test -u 自动更新测试结果。

等等!为什么自动更新后,还有一个测试用例失败了呢?

Total tests: 5, passed: 4, failed: 1.

这是因为我们忘记了修改断言测试,与 expect 测试不同,断言测试并不会自动更新。我们需要手动将断言测试对应的测试块修改为:

test {
  @assertion.assert_eq(fib(10), 89)?
}

从这个例子也能看出来,expect 测试是可以与断言测试协同工作的。

重新执行 moon test ,可以看到全部测试都通过了。

Total tests: 5, passed: 5, failed: 0.

想象一下,如果我们之前有数百个断言测试,修改起来将会非常麻烦。通过使用 expect 测试,可以让我们从枯燥的更新工作中解脱出来。

以上,我们通过两个例子展示了它们如何通过编写测试并获得即时反馈,显著提升测试效率的能力。

我们鼓励你尝试使用 MoonBit 的 expect 测试,体验其在实际应用中的便利和高效。

推荐阅读:

https://blog.janestreet.com/the-joy-of-expect-tests/

你还可以了解更多内容:

878 次点击
所在节点    推广
0 条回复

这是一个专为移动设备优化的页面(即为了让你能够在 Google 搜索结果里秒开这个页面),如果你希望参与 V2EX 社区的讨论,你可以继续到 V2EX 上打开本讨论主题的完整版本。

https://www.v2ex.com/t/1033025

V2EX 是创意工作者们的社区,是一个分享自己正在做的有趣事物、交流想法,可以遇见新朋友甚至新机会的地方。

V2EX is a community of developers, designers and creative people.

© 2021 V2EX