使用 Insta 進行 Rust 快照測試

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 可以處理你需要完成的大部分樣板代碼。

本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源https://mp.weixin.qq.com/s/Mp_wL20NELBZP55D-TJMjA