33 个版本 (20 个重大更新)
0.22.0 | 2024 年 8 月 4 日 |
---|---|
0.21.0 | 2024 年 6 月 1 日 |
0.20.0 | 2024 年 5 月 30 日 |
0.18.2 | 2023 年 8 月 13 日 |
0.2.2 | 2018 年 10 月 18 日 |
#4 在 测试 中
1,330,707 每月下载量
在 1,038 个 包中使用 1,018 个直接使用
100KB
1K SLoC
Rust 的基于固定件的测试框架
简介
rstest
使用过程宏来帮助您编写固定件和基于表格的测试。要使用它,请在您的 Cargo.toml
文件中添加以下行
[dev-dependencies]
rstest = "0.22.0"
功能
async-timeout
: 为async
测试提供timeout
(默认启用)crate-name
: 使用不同的名称导入rstest
包 (默认启用)
固定件
核心思想是您可以通过传递它们作为测试参数来注入测试依赖项。在下面的示例中,定义了一个 fixture
并在两个测试中使用它,只需简单地提供它作为参数即可
use rstest::*;
#[fixture]
pub fn fixture() -> u32 { 42 }
#[rstest]
fn should_success(fixture: u32) {
assert_eq!(fixture, 42);
}
#[rstest]
fn should_fail(fixture: u32) {
assert_ne!(fixture, 42);
}
参数化
您还可以通过其他方式注入值。例如,您可以通过为每个情况提供注入的值来创建一组测试:rstest
将为每个情况生成一个独立的测试。
use rstest::rstest;
#[rstest]
#[case(0, 0)]
#[case(1, 1)]
#[case(2, 1)]
#[case(3, 2)]
#[case(4, 3)]
fn fibonacci_test(#[case] input: u32, #[case] expected: u32) {
assert_eq!(expected, fibonacci(input))
}
在这种情况下,运行 cargo test
执行五个测试
running 5 tests
test fibonacci_test::case_1 ... ok
test fibonacci_test::case_2 ... ok
test fibonacci_test::case_3 ... ok
test fibonacci_test::case_4 ... ok
test fibonacci_test::case_5 ... ok
test result: ok. 5 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out
如果您只需为需要运行测试的一组值提供值,则可以使用 #[values)
属性参数
use rstest::rstest;
#[rstest]
fn should_be_invalid(
#[values(None, Some(""), Some(" "))]
value: Option<&str>
) {
assert!(!valid(value))
}
或通过使用 值的列表 为一些变量创建一个 矩阵 测试,这将生成所有值的笛卡尔积。
在更多测试中使用参数化定义
如果您需要为多个测试使用同一个测试列表,可以使用rstest_reuse
crate。使用这个辅助crate,您可以在任何地方定义一个模板并使用它。
use rstest::rstest;
use rstest_reuse::{self, *};
#[template]
#[rstest]
#[case(2, 2)]
#[case(4/2, 2)]
fn two_simple_cases(#[case] a: u32, #[case] b: u32) {}
#[apply(two_simple_cases)]
fn it_works(#[case] a: u32, #[case] b: u32) {
assert!(a == b);
}
有关详细信息,请参阅rstest_reuse
。
特性标记的案例
如果您希望某些测试案例只在启用特定特性时存在,请使用#[cfg_attr(feature = …, case(…))]
use rstest::rstest;
#[rstest]
#[case(2, 2)]
#[cfg_attr(feature = "frac", case(4/2, 2))]
#[case(4/2, 2)]
fn it_works(#[case] a: u32, #[case] b: u32) {
assert!(a == b);
}
这同样适用于rstest_reuse
。
魔法转换
如果您需要一个实现了FromStr()
特质的值,可以使用一个字面字符串来构建它
# use rstest::rstest;
# use std::net::SocketAddr;
#[rstest]
#[case("1.2.3.4:8080", 8080)]
#[case("127.0.0.1:9000", 9000)]
fn check_port(#[case] addr: SocketAddr, #[case] expected: u16) {
assert_eq!(expected, addr.port());
}
您也可以在值列表和测试用例默认值中使用此功能。
异步
rstest
提供了开箱即用的async
支持。只需将您的测试函数标记为async
,并使用#[async-std::test]
来注解它。这个特性对于使用整洁语法构建异步参数化测试非常有用
use rstest::*;
#[rstest]
#[case(5, 2, 3)]
#[should_panic]
#[case(42, 40, 1)]
async fn my_async_test(#[case] expected: u32, #[case] a: u32, #[case] b: u32) {
assert_eq!(expected, async_sum(a, b).await);
}
目前,开箱即支持的是async-std
。但如果您需要使用其他提供自己测试属性的运行时(例如,tokio::test
或actix_rt::test
),您可以在您的async
测试中使用它,就像在注入测试属性中描述的那样。
要使用此功能,您需要启用attributes
在您的Cargo.toml
中的async-std
功能列表。
async-std = { version = "1.5", features = ["attributes"] }
如果您的测试输入是一个异步值(测试用例或测试参数),您可以使用#[future]
属性来移除impl Future<Output = T>
样板,并直接使用T
use rstest::*;
#[fixture]
async fn base() -> u32 { 42 }
#[rstest]
#[case(21, async { 2 })]
#[case(6, async { 7 })]
async fn my_async_test(#[future] base: u32, #[case] expected: u32, #[future] #[case] div: u32) {
assert_eq!(expected, base.await / div.await);
}
正如你所注意到的,你应该使用 .await
等待所有 future 值,这有时可能非常无聊。在这种情况下,你可以使用 #[future(awt)]
来 awaiting 一个输入或使用 #[awt]
属性来全局 .await
所有你的 future 输入。之前的代码可以简化如下
use rstest::*;
# #[fixture]
# async fn base() -> u32 { 42 }
#[rstest]
#[case(21, async { 2 })]
#[case(6, async { 7 })]
#[awt]
async fn global(#[future] base: u32, #[case] expected: u32, #[future] #[case] div: u32) {
assert_eq!(expected, base / div);
}
#[rstest]
#[case(21, async { 2 })]
#[case(6, async { 7 })]
async fn single(#[future] base: u32, #[case] expected: u32, #[future(awt)] #[case] div: u32) {
assert_eq!(expected, base.await / div);
}
文件路径作为输入参数
如果你需要为给定位置的每个文件创建一个测试,可以使用 #[files("glob 路径语法")]
属性来生成满足给定 glob 路径的每个文件的测试。
#[rstest]
fn for_each_file(#[files("src/**/*.rs")] #[exclude("test")] path: PathBuf) {
assert!(check_file(&path))
}
默认行为是忽略以 "."
开头的文件,但你可以通过使用 #[include_dot_files]
属性来修改这一点。可以在同一变量上多次使用 files
属性,并且你还可以使用 #[exclude("regex")]
属性来创建一些自定义排除规则,过滤掉所有符合正则表达式的路径。
默认超时时间
你可以使用环境变量 RSTEST_TIMEOUT
为测试设置默认超时时间。该值以秒为单位,在测试编译时进行评估。
使用 #[timeout()]
你可以使用 #[timeout(<duration>)]
属性为你的测试定义执行超时时间。超时对同步和异步测试都适用,且与运行时不相关。 #[timeout(<duration>)]
接收一个表达式,该表达式应该返回一个 std::time::Duration
。以下是一个简单的异步示例
use rstest::*;
use std::time::Duration;
async fn delayed_sum(a: u32, b: u32,delay: Duration) -> u32 {
async_std::task::sleep(delay).await;
a + b
}
#[rstest]
#[timeout(Duration::from_millis(80))]
async fn single_pass() {
assert_eq!(4, delayed_sum(2, 2, ms(10)).await);
}
在这种情况下,测试通过,因为延迟仅为10毫秒,超时为80毫秒。
您可以将 timeout
属性像其他任何属性一样用于测试中,并且可以覆盖组超时以使用特定案例的超时。在下面的示例中,我们有 3 个测试,其中第一个和第三个使用 100 毫秒,但第二个使用 10 毫秒。在这个示例中另一个有价值的点是使用表达式来计算持续时间。
fn ms(ms: u32) -> Duration {
Duration::from_millis(ms.into())
}
#[rstest]
#[case::pass(ms(1), 4)]
#[timeout(ms(10))]
#[case::fail_timeout(ms(60), 4)]
#[case::fail_value(ms(1), 5)]
#[timeout(ms(100))]
async fn group_one_timeout_override(#[case] delay: Duration, #[case] expected: u32) {
assert_eq!(expected, delayed_sum(2, 2, delay).await);
}
如果您想在 async
测试中使用 timeout
,您需要使用 async-timeout
功能(默认启用)。
注入测试属性
如果您想在测试中使用其他 test
属性,您只需在测试函数的属性中指定它即可。例如,如果您想使用 actix_rt::test
属性测试一些异步函数,您只需写下:
use rstest::*;
use actix_rt;
use std::future::Future;
#[rstest]
#[case(2, async { 4 })]
#[case(21, async { 42 })]
#[actix_rt::test]
async fn my_async_test(#[case] a: u32, #[case] #[future] result: u32) {
assert_eq!(2 * a, result.await);
}
只有以 test
结尾的属性(最后一个路径段)可以注入。
使用 #[once]
固定值
如果您需要为所有测试只初始化一次的固定值,您可以使用 #[once]
属性。 rstest
只调用一次您的固定值函数,并返回对您的函数结果的引用到所有测试中
#[fixture]
#[once]
fn once_fixture() -> i32 { 42 }
#[rstest]
fn single(once_fixture: &i32) {
// All tests that use once_fixture will share the same reference to once_fixture()
// function result.
assert_eq!(&42, once_fixture)
}
局部生命周期和 #[by_ref]
属性
在某些情况下,您可能希望为测试的一些参数使用局部生命周期。在这些情况下,您可以使用 #[by_ref]
属性,然后使用引用而不是值。
enum E<'a> {
A(bool),
B(&'a Cell<E<'a>>),
}
fn make_e_from_bool<'a>(_bump: &'a (), b: bool) -> E<'a> {
E::A(b)
}
#[fixture]
fn bump() -> () {}
#[rstest]
#[case(true, E::A(true))]
fn it_works<'a>(#[by_ref] bump: &'a (), #[case] b: bool, #[case] expected: E<'a>) {
let actual = make_e_from_bool(&bump, b);
assert_eq!(actual, expected);
}
您可以为测试的所有参数使用 #[by_ref]
属性,而不仅仅是固定值,还包括案例、值和文件。
完整示例
所有这些功能都可以与固定值变量、固定案例和一系列值混合使用。例如,您可能需要两个测试案例来测试恐慌,一个用于已登录用户,另一个用于访客用户。
use rstest::*;
#[fixture]
fn repository() -> InMemoryRepository {
let mut r = InMemoryRepository::default();
// fill repository with some data
r
}
#[fixture]
fn alice() -> User {
User::logged("Alice", "2001-10-04", "London", "UK")
}
#[rstest]
#[case::authorized_user(alice())] // We can use `fixture` also as standard function
#[case::guest(User::Guest)] // We can give a name to every case : `guest` in this case
// and `authorized_user`
#[should_panic(expected = "Invalid query error")] // We would test a panic
fn should_be_invalid_query_error(
repository: impl Repository,
#[case] user: User,
#[values(" ", "^%$some#@invalid!chars", ".n.o.d.o.t.s.")] query: &str,
) {
repository.find_items(&user, query).unwrap();
}
此示例将生成正好 6 个测试,按 2 个不同的案例分组
running 6 tests
test should_be_invalid_query_error::case_1_authorized_user::query_1_____ - should panic ... ok
test should_be_invalid_query_error::case_2_guest::query_2_____someinvalid_chars__ - should panic ... ok
test should_be_invalid_query_error::case_1_authorized_user::query_2_____someinvalid_chars__ - should panic ... ok
test should_be_invalid_query_error::case_2_guest::query_3____n_o_d_o_t_s___ - should panic ... ok
test should_be_invalid_query_error::case_1_authorized_user::query_3____n_o_d_o_t_s___ - should panic ... ok
test should_be_invalid_query_error::case_2_guest::query_1_____ - should panic ... ok
test result: ok. 6 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s
注意,值的名称尝试将输入表达式转换为 Rust 有效的标识符名称,以帮助您找到哪些测试失败。
更多
就这么多吗?还不完全是!
固定值可以被另一个固定值注入,并且可以仅使用其一些参数调用它们。
#[fixture]
fn user(#[default("Alice")] name: &str, #[default(22)] age: u8) -> User {
User::new(name, age)
}
#[rstest]
fn is_alice(user: User) {
assert_eq!(user.name(), "Alice")
}
#[rstest]
fn is_22(user: User) {
assert_eq!(user.age(), 22)
}
#[rstest]
fn is_bob(#[with("Bob")] user: User) {
assert_eq!(user.name(), "Bob")
}
#[rstest]
fn is_42(#[with("", 42)] user: User) {
assert_eq!(user.age(), 42)
}
正如您所注意到的,您可以提供默认值,而无需固定值定义它。
最后,如果您需要跟踪输入值,只需将 trace
属性添加到您的测试中即可启用所有输入变量的转储。
#[rstest]
#[case(42, "FortyTwo", ("minus twelve", -12))]
#[case(24, "TwentyFour", ("minus twentyfour", -24))]
#[trace] //This attribute enable tracing
fn should_fail(#[case] number: u32, #[case] name: &str, #[case] tuple: (&str, i32)) {
assert!(false); // <- stdout come out just for failed tests
}
running 2 tests
test should_fail::case_1 ... FAILED
test should_fail::case_2 ... FAILED
failures:
---- should_fail::case_1 stdout ----
------------ TEST ARGUMENTS ------------
number = 42
name = "FortyTwo"
tuple = ("minus twelve", -12)
-------------- TEST START --------------
thread 'should_fail::case_1' panicked at 'assertion failed: false', src/main.rs:64:5
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace.
---- should_fail::case_2 stdout ----
------------ TEST ARGUMENTS ------------
number = 24
name = "TwentyFour"
tuple = ("minus twentyfour", -24)
-------------- TEST START --------------
thread 'should_fail::case_2' panicked at 'assertion failed: false', src/main.rs:64:5
failures:
should_fail::case_1
should_fail::case_2
test result: FAILED. 0 passed; 2 failed; 0 ignored; 0 measured; 0 filtered out
如果有一个或多个变量没有实现 Debug
特性,则会引发错误,但也可以使用 #[notrace]
参数属性来排除变量。
您可以在 文档 中了解更多信息,并在 tests/resources
目录中找到更多示例。
Rust 版本兼容性
支持的最小Rust版本是1.67.1。
变更日志
请参阅CHANGELOG.md
许可证
在以下许可证之一下授权:
-
Apache License, Version 2.0, (LICENSE-APACHE或license-apache-link)
-
MIT许可证LICENSE-MIT或license-MIT-link,任选其一。
依赖项
~2.6–4.5MB
~80K SLoC