“
原文链接: https://blog.x5ff.xyz/blog/async-tests-tokio-rust/
原文标题: TWO EASY WAYS TO TEST ASYNC FUNCTIONS IN RUST
公众号: Rust碎碎念
喜欢Rust的一个原因就是它的测试。不需要安装测试运行程序,不要研读10种不同的单元测试框架,没有兼容性问题...
或者还有其他的问题?在历经数年的开发[1]之后,Rust最近开始支持完整的async/await,但是看起来好像少了一点东西:测试(tests),怎么办?
让我们回顾一下(LET’S BACK UP A BIT)
为了能使用异步代码,你需要两个东西:
- 一个运行时(比如 tokio[2])
async函数
后者完全在你的掌控之内,只需要做一些语法上的修改(如果你已经熟悉了异步模式(async paradigm)[3] )。那么运行时呢?选择运行时-比如在Scala中-是自动完成的并且可以工作,但是这不是Rust团队想要的。正如Rust的许多其他方面(例如,内存分配),重点是能够调整为你的程序提供支持的技术;这一点对于嵌入式编程来说特别有价值。
tokio似乎是最流行的运行时并且它为很多著名的框架提供支持。也有一些框架带有自建的运行时(比如actix-web[4])。这些都比较简单,通过它们提供的例子,你可以快速上手。
关于测试(WHAT ABout TESTS)
测试是软件工程(不同于编程/脚本/...)的重要组成部分,在软件工程中,开发者想要确保他/她所写的东西是可读的以便于将来的维护、扩展或者是赏心悦目。通常情况下,很多项目缺乏易于使用的测试设置,导致其缺少结构化测试...
这就引出了现在的话题:怎么测试你的异步函数?Rust内置的测试没有自带一个运行时,所以如果你开始在普通测试中调用你的异步函数,事情会变得棘手。编译器如何知道一个特定的异步代码块应该在哪个线程上运行?什么时候结果会到来?是否应该等待结果?
让我们用一个例子来说明:
fn str_len(s: &str) -> usize {
s.len()
}
async fn str_len_async(s: &str) -> usize {
// do something awaitable ideally...
s.len()
}
#[cfg(test)]
#[allow(non_snake_case)]
mod tests {
use super::*;
#[test]
fn test_str_len() {
assert_eq!(str_len("x5ff"), 4);
}
}
如果str_len()是一个普通函数,不会有什么问题。但是str_len_async()是异步的并且我们不能像同步测试那样进行测试。我们必须考虑如何最好的对函数返回的Future进行unwrap:
$ cargo test
Compiling async-testing v0.1.0 (/private/tmp/async-testing)
error[E0369]: binary operation `==` cannot be applied to type `impl std::future::Future`
--> src/lib.rs:23:9
|
23 | assert_eq!(str_len_async("x5ff"), 4);
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
| |
| impl std::future::Future
| {integer}
|
= note: this error originates in a macro (in Nightly builds, run with -Z macro-backtrace for more info)
error[E0277]: `impl std::future::Future` doesn't implement `std::fmt::Debug`
--> src/lib.rs:23:9
|
23 | assert_eq!(str_len_async("x5ff"), 4);
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ `impl std::future::Future` cannot be formatted using `{:?}` because it doesn't implement `std::fmt::Debug`
|
= help: the trait `std::fmt::Debug` is not implemented for `impl std::future::Future`
= note: required because of the requirements on the impl of `std::fmt::Debug` for `&impl std::future::Future`
= note: required by `std::fmt::Debug::fmt`
= note: this error originates in a macro (in Nightly builds, run with -Z macro-backtrace for more info)
error: aborting due to 2 previous errors
Some errors have detailed explanations: E0277, E0369.
For more information about an error, try `rustc --explain E0277`.
error: could not compile `async-testing`.
To learn more, run the command again with --verbose.
如果你没有跳过介绍部分,现在你应该知道为什么会这样以及应该做什么:增加一个运行时。
异步测试的两种方式(TWO WAYS TO DO ASYNC TESTING)
至少有两种运行异步测试的方式,具体取决于你的偏好以及你对依赖有多挑剔(不必要)。而且,要知道Rust的测试默认是多线程的,所以和普通程序相比,异步运行时在这里的作用有限。
1.使用框架的测试支持(USE YOUR FRAMEWORK’S TESTING SUPPORT)
Actix带有很多例子,并且其中一个也有异步测试的功能。作为其设计的一部分,他们在函数之上使用了一个额外的属性,将其分配给一个运行时,运行异步测试就像运行其他的异步代码一样。
// ...
#[cfg(test)]
#[allow(non_snake_case)]
mod tests {
use super::*;
#[test]
fn test_str_len() {
assert_eq!(str_len("x5ff"), 4);
}
#[actix_rt::test]
async fn test_str_len_async() {
assert_eq!(str_len_async("x5ff").await, 4);
}
}
这使得异步函数可以像平时一样(在测试中)使用,但是需要actix-rt[5]作为依赖:
[dependencies]
actix-rt = "*"
对于那些非web的项目,还有另一种方式: 使用一个专门的测试运行时。
2. 使用测试运行时(USE A TESTING RUNTIME)
如果你想使用一个不同的框架或者在不同于web服务器的用例中测试,还有另一种方式。tokio-test[6]提供了一个测试运行时。这个运行时提供了一个函数来"反转(reverse) "异步函数的工作,并阻塞执行,直到异步函数返回。这个函数叫做tokio_test::block_on()(文档[7])并且它阻塞当前的线程直到Future结束执行。
为了减少打字的工作量,我建议创建像这样的一个宏(aw是await的缩写):
macro_rules! aw {
($e:expr) => {
tokio_test::block_on($e)
};
}
如果我们把这个测试添加到上面的例子中:
// ...
#[cfg(test)]
#[allow(non_snake_case)]
mod tests {
use super::*;
#[test]
fn test_str_len() {
assert_eq!(str_len("x5ff"), 4);
}
// ... the other async test
macro_rules! aw {
($e:expr) => {
tokio_test::block_on($e)
};
}
#[test]
fn test_str_len_async_2() {
assert_eq!(aw!(str_len_async("x5ff")), 4);
}
}
显然,tokio-test必须要被添加到你的依赖项列表中,但是它可以放到dev-dependencies部分,在这个部分里,它不会增加另一层依赖:
[dev-dependencies]
tokio-test = "*"
所以,现在你可以快速关闭测试循环,写出经过良好测试的软件。
完成(DONE)?
你应该看到这些:
$ cargo test
Finished test [unoptimized + debuginfo] target(s) in 0.17s
Running target/debug/deps/async_testing-7d7ff38dac475e0e
running 3 tests
test tests::test_str_len ... ok
test tests::test_str_len_async_2 ... ok
test tests::test_str_len_async ... ok
test result: ok. 3 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out
Doc-tests async-testing
running 0 tests
test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out
如果不是这样,去GitHub仓库看一下完整的代码。
最后一件事:如果你认为这是有价值的,请把他分享给能从中收益的人。
本文禁止转载,谢谢配合!欢迎关注我的微信公众号: Rust碎碎念
谢谢!
参考资料
[1]数年的开发: https://blog.rust-lang.org/2019/11/07/Async-await-stable.html
[2]tokio: https://tokio.rs/
[3]异步模式(async paradigm ): https://docs.microsoft.com/en-us/dotnet/csharp/programming-guide/concepts/async/
[4]actix-web: https://actix.rs/
[5]actix-rt: https://crates.io/crates/actix-rt
[6]tokio-test: https://crates.io/crates/tokio-test
[7]文档: https://docs.rs/tokio-test/0.2.1/tokio_test/fn.block_on.html