使用Insta进行Rust快照测试

606次阅读  |  发布于11月以前

Rust有很多测试策略,从单元测试到集成测试。在本文中,我们将探索使用Insta进行快照测试,了解它如何补充你的开发工作。

什么是快照测试?

快照测试是一种通过将输出与一组已定义的预期值进行比较来验证代码正确性的方法。例如,如果你以前编写过集成测试,那么可以将部分测试视为快照,因为你正在将预期结果与实际输出进行比较。

默认情况下,Rust使用assert_eq!函数,但它只允许你与原始Rust类型进行比较。快照测试要求对更复杂的数据结构进行比较。

通常,快照测试是在前端而不是后端完成的,因为前端应用程序返回HTML而不是常规字符串。比较输出更省时,而不是解析HTML并检查每个特定元素。

在测试整个程序的输出时,你可以充分利用快照测试,从而测试网页中的更多元素,而不必担心所有结果是否一致。

Insta是什么?

Insta是一个Rust应用程序的快照测试库,提供了一个简单而直观的界面来运行和更新测试:

正如上面的截图中看到的,在Insta中调试测试非常容易,并且在Insta CLI的帮助下,你可以很容易地用新的测试输出更新所有失败的测试输出。请记住,不应该在每次失败时都更新结果。应该只在更改某个测试的代码输出时才实现更新,从而将代码更新导致的错误数量降至最低。

Insta仅通过Serde支持CSV、JSON、TOML、YAML和RON文件,Serde是一个数据序列化库,可以将各种类型的数据结构编码为更紧凑的格式,反之亦然。

Insta如何工作的?

Insta有许多不同类型的支持。如前所述,可以使用Insta对JSON文件、CSV文件甚至YAML文件进行快照测试。但有趣的是,Insta宏是如何在底层运行的。

Insta使用Serde提供多文件支持。不过,Insta并没有将它们分割成更小的包,而是依靠Cargo的功能将所有包无缝地打包为一个包,因此客户端可以通过这些功能只下载他们需要的包:

// Cargo.toml
[features]
csv = ["dep_csv", "serde"]
json = ["serde"]
ron = ["dep_ron", "serde"]
toml = ["dep_toml", "serde"]
yaml = ["serde"]

Insta快照断言库只比较两个字符串。因此,只需要传递SerializationFormat,assert_snapshot!宏将编译和正常工作。

Insta VS. assert_eq

在底层,Insta和assert_eq都做序列化以外的事情。这两种断言解决方案之间最大的区别是Insta本地支持序列化。而在使用assert_eq!时,必须使用Serde进行手动序列化,以实现与Insta相同的结果。

即使在assert_snapshot函数内部,Insta也会进行简单的字符串与字符串比较。使用assert_eq将实现类似的结果。assert_eq的比较过程比Insta的要轻量级得多,与直接使用Insta相比,比较Insta和assert_eq并不理想,因为它需要大量的样板代码和额外的工作。

开始使用Insta

使用Insta很简单,像任何其他库一样,打开Cargo.toml文件。并添加相关依赖项。如下所示,将同时添加Insta和Serde:

// Cargo.toml // ...
[features]
json = ["serde"]

[dependencies]
serde = { version = "1.0.117", optional = true }

[dev-dependencies]
insta = { version = "1.26.0", features = ["json", "csv"] }
serde = { version = "1.0.117", features = ["derive"] }

在本例中,我们将使用json和csv特性来编写一个简单的程序,供你测试。我们将创建一个简单的待办事项列表CLI应用程序来跟踪任务。

首先,在src/main.rs文件中创建一个基本样板:

use std::env;
use std::io::{self, BufRead};
use std::path::Path;

struct Task {
    name: String,
    is_completed: bool,
}

fn readline() -> String {
    let mut strr: String = "".to_string();
    let stdin = io::stdin();
    for line in stdin.lock().lines() {
        strr = line.unwrap().to_string();
        break;
    }
    strr
}

fn main() -> io::Result<()> {
    let mut tasks: Vec<Task> = vec![];
    while true {
        println!("{}", readline());
        break;
    }
    Ok(())
}

fn add_task(tasks: &Vec<Task>, name: String) -> io::Result<()> {
    // TODO: Add logic
    Ok(())
}

fn list_tasks(tasks: &Vec<Task>) -> io::Result<()> {
    // TODO: List logic
    Ok(())
}

fn complete_task(tasks: &Vec<Task>, level: i32) -> io::Result<()> {
    // TODO: Complete logic
    Ok(())
}

这个简单的CLI允许用户创建或更新新任务。代码的结构可能会让你了解我们将使用Insta做什么。但是,我们先别太超前了。

接下来,我们将定义每个函数以添加更多结构,重点关注main函数:

fn main() -> io::Result<()> {
    let mut tasks: Vec<Task> = vec![];
    loop {
        list_tasks(&tasks);

        let option = readline();
        _ = match option.as_str() {
            "1" => {
                println!("Enter new task name: ");
                let name = readline();
                add_task(&mut tasks, name);
            },
            "2" => {
                println!("Enter task to complete: ");
                let level: i32 = readline().parse::<i32>().unwrap();
                complete_task(&mut tasks, level);
            },
            _ => break,
        };
    }
    Ok(())
}

main函数将命令重定向到应用程序的其他函数,可以列出任务、创建新任务或完成任务。为了简化事情,现在我们还没有任务移除函数。但是,如果需要,可以稍后实现。

由于可变tasks向量是从main函数传递给add_task函数的,所以你可以使用.push修饰符向向量中添加一个新task:

fn add_task(tasks: &mut Vec<Task>, name: String) -> io::Result<()> {
    tasks.push(Task {
        name: name,
        is_completed: false,
    });
    Ok(())
}

每次打开CLI时,都需要列出任务列表。List_tasks已经在main函数中的循环开始时声明;你所需要做的就是定义它。为了简化,将任务向量传递给list_tasks函数。然后,遍历它们并打印它们的名称和状态:

fn list_tasks(tasks: &Vec<Task>) {
    for _ in 0..50 {
        println!("\n");
    }
    println!("Tasks List: ");
    for task in tasks {
        println!("Name: {}", task.name);
        println!("Is Completed: {}", task.is_completed);
    }
    println!("Choose the following options:
1. Add tasks
2. Complete tasks
3. Exit");
}

Rust没有为cli提供清晰的屏幕选项,你可以通过打印50次换行来解决这个问题。

最后,要更新任务,只需更新它们的状态。可以直接访问vector对象并修改is_completed属性:

fn complete_task(tasks: &mut Vec<Task>, level: i32) -> io::Result<()> {
    tasks[level as usize].is_completed = true;
    Ok(())
}

现在,尝试运行应用程序,应该能够创建和完成新的任务。输入cargo run,应该看到如下内容:

Tasks List: 
Choose the following options:
1. Add tasks
2. Complete tasks
3. Exit

它不是最复杂的应用程序,但对于我们的教程,它是可行的。输入1并按回车,将任务重命名为new task,将收到以下信息:

Tasks List: 
Name: new task
Is Completed: false
Choose the following options:
1. Add tasks
2. Complete tasks
3. Exit

现在已经验证了一切都在运行,可以继续进行快照测试了。

Serde是可选依赖项,Insta是开发依赖项,所以不能将它们包含在主应用程序上下文中。必须在它们前面加上一个#[cfg(test)]宏。多个Task结构如下所示:

#[cfg(test)]
#[derive(serde::Serialize, Clone)]
struct Task {
    pub name: String,
    pub is_completed: bool,
}

#[cfg(not(test))]
#[derive(Clone)]
struct Task {
    pub name: String,
    pub is_completed: bool,
}

测试中使用的Task将是可序列化和可克隆的,因此我们可以存储同一对象的多个副本而不会破坏它。我们将在测试时使用#[cfg(test)]结构,在使用cargo run运行项目时使用#[cfg(not(test))]结构。我们将为不同的上下文分离结构;虽然这不是最好的做法,但它会节省时间来专注于更重要的Insta测试。

为了使add_task和complete_task可测试,它们必须在每次运行时返回一个Task结构体:

fn add_task(tasks: &mut Vec<Task>, name: String) -> io::Result<Task> {
    let task = Task {
        name,
        is_completed: false,
    };
    tasks.push(task.clone());
    Ok(task)
}

// fn list_tasks....

fn complete_task(tasks: &mut Vec<Task>, level: i32) -> io::Result<Task> {
    tasks[level as usize].is_completed = true;
    Ok(tasks[level as usize].clone())
}

前面添加的Clone派生将用于add_task函数。使用Insta编写这个函数的单元测试,在文件的底部,添加以下代码:

#[cfg(test)]
extern crate insta;

然后,像这样测试add_task函数:

#[cfg(test)]
mod tests {
    use super::*;
    use insta::{assert_json_snapshot, assert_compact_json_snapshot, assert_csv_snapshot};

    #[test]
    fn test_json_add_task_struct_vec() {
        let mut tasks: Vec<Task> = vec![];

        let task: Task = add_task(&mut tasks, "name".to_string()).unwrap();
        assert_json_snapshot!(task, @r###"{
  "name": "name",
  "is_completed": false
}"###);
        assert_compact_json_snapshot!(task, @r###"{"name": "name", "is_completed": false}"###);

        assert_json_snapshot!(tasks, @r###"[
  {
    "name": "name",
    "is_completed": false
  }
]"###);
        assert_compact_json_snapshot!(tasks, @r###"[{"name": "name", "is_completed": false}]"###);
    }
}

通过在终端或命令提示符中执行cargo test命令来运行测试。所有测试都应该成功通过,这样就完成了 !

总结

当开发人员编写应用程序时,他们会面临测试的挑战。在理想的情况下,我们能够运行我们的代码,并确保在将其部署到生产环境之前按预期工作。然而,现实世界的软件开发远非理想,因此测试是我们开发工作流程的重要组成部分。在开发任务关键型系统或应用程序时尤其如此,在这些系统或应用程序中,失败是不可避免的。

除了使用C/C++之外,Rust等提供严格类型系统的语言正日益成为任务关键型应用程序的一种选择,特别是在考虑速度和内存安全的情况下。

快照测试通过验证输出来帮助你验证代码的正确性。如果你正在管理一个不断变化的代码库,这是非常有用的,这样就可以在进行更新或更改时发现是否有什么问题。

Insta提供了围绕常见断言宏的包装器,供在程序中使用。通过对它们进行测试,并根据项目需求为部署做好准备,Insta可以处理你需要完成的大部分样板代码。

Copyright© 2013-2020

All Rights Reserved 京ICP备2023019179号-8