给 rust 程序写测试案例

rust / cargo

总结了下 rust 写测试案例和测试时近期遇到的一些初步的用法,为以后的使用减少一些疑惑

如何写测试案例

每个源文件都可以写测试案例,这样案例和源码在一起,方便边测边开发调试的情况。写测试案例的格式也很简单,只要在源文件中加入以下内容:

#[cfg(test)]
mod tests {

    #[test]
    pub fn screen_test() {

    }
}

简单总结就是:

  1. 在每个你想写测试案例的源文件中,增加一个 mod,给 mod 增加注解 #[cfg(test)] 表示这个 mod 只在测试时执行。
  2. 每一个有 #[test] 注解的函数就是一个测试案例。

没了,就这么多,当然你也可以把源码和测试案例分开放,这是我个人的喜好, 一般情况下我们可以通过 IDE 或编辑器的集成功能来运行和调试测试案例。

Untitled

注意这里的 run 和 debug 根据我的经验都是 debug 模式运行的,差别只是在于一个进断点,一个不进。 因为我之前有写过循环次数比较多的案例,点 run test 跑了好久,但是用下面的方法,很快就完成了。

命令行执行测试案例

可能大部分初学者都会知道,命令行执行测试案例是用 cargo test。但是这还是没有优化过的程序, 如果想让案例全速执行,你需要加一个参数:

cargo test --release

那么在这个基础上,如果我想执行某个测试案例呢,可能有人会说是这样:

cargo test --release tests::screen_test

也对,也不对,对于我们这个例子是没错,但是如果你有这样几个测试案例:

#[cfg(test)]
mod tests {

    #[test]
    pub fn screen_test() {}
    #[test]
    pub fn screen_test1() {}
    #[test]
    pub fn screen_test2() {}
}

这几个案例都会执行,因为 cargo 会在所有源文件中寻找所有匹配的案例, 如果我们只想运行 screen_testcargo 提供了一个参数 —exact, 这个参数用起来也很讲究,需要放在 — 后面:

cargo test --release tests::screen_test -- --exact

很奇怪对不对,熟悉一点朋友可能会知道这个 — 的用意,它是区分 cargo 和编译后程序的参数, 换句话说, 前的参数是 cargo 的,后面是 test 的, 我们一般用这种办法向命令行程序传递参数,不过测试案例也可以放后面传,这样在我看来会舒服一点:

cargo test --release -- tests::screen_test --exact

但是我们直接用的话,很可能会发现案例并没有执行:

running 0 tests

test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 2 filtered out; finished in 0.00s

     Running unittests src/main.rs (target/release/deps/rich3kit-8cbea6fb2e1f2f7b)

running 0 tests

test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 2 filtered out; finished in 0.00s

     Running tests/test.rs (target/release/deps/test-e28c8f1f83a0899d)

running 0 tests

test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 2 filtered out; finished in 0.00s

   Doc-tests rich3kit

running 0 tests

test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s

一堆 0,仰仗 exact 这个参数,test 干脆不干活了,不过简单观察一下,我们可能就能推测出, 这是因为它没有找到这个案例,原因自然是因为我们没有在 cargo test 的命令中体现出源文件的路径, 我们需要这么写:

cargo test --release -- cmd::screen_cmd::tests::screen_test --exact
    Finished release [optimized] target(s) in 0.31s
    Running unittests src/lib.rs (target/release/deps/rich3kit-9b4b0afca785b931)

running 1 test
test cmd::screen_cmd::tests::screen_test ... ok

test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 3 filtered out; finished in 0.00s

     Running unittests src/main.rs (target/release/deps/rich3kit-8cbea6fb2e1f2f7b)

running 1 test
test cmd::screen_cmd::tests::screen_test ... ok

test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 3 filtered out; finished in 0.00s

     Running tests/test.rs (target/release/deps/test-e28c8f1f83a0899d)

running 0 tests

test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 2 filtered out; finished in 0.00s

   Doc-tests rich3kit

running 0 tests

test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.01s

这下对了,不过我有种这个测试跑了两次的感觉,我们核实一下,修改下我们的案例:

static I: AtomicU32 = AtomicU32::new(0);

#[test]
pub fn screen_test() {
    let i = I.fetch_add(1, Ordering::SeqCst) + 1;
    assert_eq!(1, i);
}

如果跑了两次会有一次失败,我们再试试看:

cargo test --release -- cmd::screen_cmd::tests::screen_test --exact
    Finished release [optimized] target(s) in 0.30s
     Running unittests src/lib.rs (target/release/deps/rich3kit-9b4b0afca785b931)

running 1 test
test cmd::screen_cmd::tests::screen_test ... ok

test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 3 filtered out; finished in 0.00s

     Running unittests src/main.rs (target/release/deps/rich3kit-8cbea6fb2e1f2f7b)

running 1 test
test cmd::screen_cmd::tests::screen_test ... ok

test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 3 filtered out; finished in 0.00s

     Running tests/test.rs (target/release/deps/test-e28c8f1f83a0899d)

running 0 tests

test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 2 filtered out; finished in 0.00s

   Doc-tests rich3kit

running 0 tests

test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s

看来虽然显示像是跑了两次,但是实际上还是一次,事实上呢其实是分别跑了三次, 一次是为 lib.rs 跑的,一次是为 main.rs 跑的,还有一次为 test.rs, 但是只有前两个有对应到我们的测试案例,这个和代码结构有关就不再展开了, 不过我们的案例是确定每次只跑了一遍没错。

总结

原以为只是一个简单的总结,方便后续查找,没想到越展开越多了,简单总结一下:

  1. 每个源文件都可以写案例,mod 前加 #[cfg(test)] 注解

  2. 每个加 #[test] 注解的函数都是测试案例

  3. 执行一个具体案例的范例:

    cargo test --release -- cmd::screen_cmd::tests::screen_test --exact