【译】Rust中测试异步函数的两种方式

4,389 阅读3分钟

原文链接: 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)

为了能使用异步代码,你需要两个东西:

  1. 一个运行时(比如 tokio[2])
  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::Debugfor `&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").await4);
  }
}

这使得异步函数可以像平时一样(在测试中)使用,但是需要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碎碎念

Rust碎碎念
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