1 个不稳定发布
0.1.0 | 2022 年 11 月 11 日 |
---|
#33 in #stored
13KB
53 行
执行针对存储在外部文件中的测试向量的测试
宏 [macro@test_vectors] 用于注释一个 测试标准函数,该函数对多个 案例 进行执行。每个案例扩展为一个独立的 Rust 单元测试(即一个 #[test]
函数)。每个案例的数据存储在一个 案例目录 中,其中测试标准函数的每个参数都与一个文件相关联。所有的案例目录都位于一个 语料库目录 中,该目录由宏 [macro@test_vectors] 的 dir
参数指定。
示例
假设我们有一个可以将字符串中的空格替换为破折号的复杂软件包,并且我们想要对这个功能对一系列输入向量文件进行测试。
我们可以这样组织软件包内容
Cargo.toml
- 根据 test-vectors 在[dev-dependencies]
中依赖src/lib.rs
- 包含下面的示例代码test-data/example1/alpha/input
- 包含这是 alpha
test-data/example1/alpha/expected
- 包含this_is_alpha
test-data/example1/beta/input
- 包含这是 beta
test-data/example1/beta/expected
- 包含this_is_beta
现在在我们的 lib.rs
中我们有
pub fn replace_spaces_with_underscores(input: &str) -> String {
input.replace(' ', "_")
}
#[test_vectors::test_vectors(
dir = "test-data/example1"
)]
fn test_replace(input: &[u8], expected: &[u8]) -> Result<(), std::str::Utf8Error> {
// Test setup:
let instr = std::str::from_utf8(input)?;
let expstr = std::str::from_utf8(expected)?;
// Application code test target:
let output = replace_spaces_with_underscores(instr);
// Test verification:
assert_eq!(expstr, &output);
Ok(())
}
这从语料库目录内的案例目录中创建了两个Rust单元测试 test-data/example1
。案例以案例目录命名,分别为 alpha
和 beta
。对于每个测试,案例目录中的 input
和 expected
文件的文件内容被映射到 &[u8]
测试标准函数参数。运行 cargo test
的输出将包含类似以下内容
test test_replace_alpha ... ok
test test_replace_beta ... ok
动机
这种设计非常适合以下功能之一的测试
- 根据输入将相同的标准函数分开成不同的案例(类似于 test-case 库)。如果某个测试标准下的案例子集失败,运行
cargo test
的输出将立即识别出特定的失败案例,这与单个#[test]
函数作为整个测试向量循环相比。 - 测试直接存储在文件中的原始未编码数据,而不是Rust特定的字面表示。这有助于避免实时生产数据与旨在表示数据的Rust字面表示之间的差异。
- 针对外部文件进行测试,这有助于对多个实现进行针对公共测试向量的 一致性测试。例如,网络协议标准可能包括一系列消息序列化测试向量,多个实现可以针对这些向量进行验证。
- 在其他外部工具上使用外部数据文件。例如,如果一个视频编解码器元数据解析库有外部测试向量文件,可以直接使用其他用于检查该视频格式的工具,如交互式视频播放器,直接在测试向量上使用。
语料库和案例目录
语料库目录由 dir
宏参数指定。这是一个相对于 CARGO_MANIFEST_DIR
环境变量的路径(其中存放 Cargo.toml
文件)。
语料库目录内的每个目录都应被视为案例目录(在遍历符号链接之后)。非目录将被忽略,并建议包含一个 README.md
文件来解释语料库。
在案例目录内,只有从标准函数参数名称派生的路径被访问,其他内容被忽略,因此建议包含一个 README.md
文件来解释案例的意图。此行为的另一个细微之处是,不同的标准函数可能会重用同一个语料库目录。
例如,位于 test-data/example2
的案例目录可能有以下文件
input
包含内容this is the input
underscores
包含内容this_is_the_input
elided
包含内容thisistheinput
然后两个不同的标准函数可以测试相同的输入的不同转换
use test_vectors::test_vectors;
use std::str::Utf8Error;
#[test_vectors(
dir = "test-data/example2"
)]
fn replace_spaces_with_underscores(input: &[u8], underscores: &[u8]) -> Result<(), Utf8Error> {
let instr = std::str::from_utf8(input)?;
let expstr = std::str::from_utf8(underscores)?;
let output = instr.replace(' ', "_");
assert_eq!(expstr, &output);
Ok(())
}
#[test_vectors(
dir = "test-data/example2"
)]
fn elide_spaces(input: &[u8], elided: &[u8]) -> Result<(), Utf8Error> {
let instr = std::str::from_utf8(input)?;
let expstr = std::str::from_utf8(elided)?;
let output = instr.replace(' ', "");
assert_eq!(expstr, &output);
Ok(())
}
由于两个标准函数都使用了相同的语料库,并且都接受 input
作为参数,因此它们测试的是相同的测试向量 input
文件,而每个函数都读取不同的测试向量来执行其特定的功能,即 underscores
与 elided
。
从字节自动转换输入
标准测试函数的参数通过 TryFrom<&[u8]>
与文件内容进行转换,这些内容可以作为 &[u8]
获取。由于这个特性提供了一个通用的实现,所以类型为 &[u8]
的参数是基本支持的类型。
对于其他类型,可以使用标准 Rust 特性来处理一些模板代码,从而进行输入转换。与支持宏接口中的自定义转换相比,这种做法通过依赖这个标准 Rust 特性,使得宏接口和逻辑更简单。
转换的结果不会进行包装,所以转换失败将导致 panic,测试用例将失败。调用站点的样子如下
<T>::try_from(include_bytes!(…)).unwrap()
在第一个例子中,我们显式调用了 std::str::from_utf8 来转换字节切片参数。这是一个无法通过 TryFrom<&[u8]>
(因为可能存在多种将字节转换为 str
的方法)来获得的转换函数的例子。因此,这个例子突出了测试 criterion 函数可能需要依赖新类型包装类型来执行转换。例如,test-vectors crate 提供了一些常用的包装类型,如 [Utf8Str]。将 [Utf8Str] 文档中的例子与上面的第一个例子进行比较。
如果测试需要一些自定义转换,可能需要实现一个自定义新类型包装器,如下一个例子所示
自定义转换新类型实现示例
假设你的 crate 类型 T
实现了 serde 的 Deserialize
特性,你的测试向量是 JSON 数据,你希望在 criterion 函数中去除反序列化 JSON 的模板代码。
你可以实现一个新类型,为你执行转换
use serde::Deserialize;
use test_vectors::test_vectors;
#[derive(Deserialize)]
struct AppType {
valid: bool
}
impl AppType {
fn is_valid(&self) -> bool {
self.valid
}
}
struct AppTypeFromJson(AppType);
impl std::ops::Deref for AppTypeFromJson {
type Target = AppType;
fn deref(&self) -> &Self::Target {
&self.0
}
}
impl TryFrom<&[u8]> for AppTypeFromJson
{
type Error = serde_json::Error;
fn try_from(input: &[u8]) -> Result<Self, Self::Error> {
serde_json::from_slice(input).map(AppTypeFromJson)
}
}
#[test_vectors(
dir = "test-data/example3"
)]
fn validate(value: AppTypeFromJson) {
// Perform test-logic on `value`:
assert!(value.is_valid());
}
criterion 函数返回类型
criterion 函数的返回类型在每个测试用例中直接复制,测试用例返回未经修改的 criterion 函数结果。criterion 函数可以返回 ()
或 [Result],具有与单元测试相同的行为。
依赖项
~2MB
~43K SLoC