#vector #testing #stored #external #execute #input #directory

test-vectors

执行针对存储在外部文件中的测试向量的测试

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。案例以案例目录命名,分别为 alphabeta。对于每个测试,案例目录中的 inputexpected 文件的文件内容被映射到 &[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 文件,而每个函数都读取不同的测试向量来执行其特定的功能,即 underscoreselided

从字节自动转换输入

标准测试函数的参数通过 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 实现了 serdeDeserialize 特性,你的测试向量是 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