端到端測試 Rust 服務

如果你正在用 Rust 構建 web API,需要一種方法來端到端測試你的端點。單元測試可以確保你的邏輯是正確的,但是適當的端到端測試可以驗證基礎設施、路由、數據庫遷移和安全設置是否正確。最好的方法之一是在 CI/CD 過程中進行端到端測試。對於 Rust 服務,Cargo 使這個過程變得輕鬆。

端到端測試的要點

使用端到端測試是從客戶的角度體驗你的服務,你不僅要驗證邏輯是否正確 (可以通過單元測試進行驗證),還要驗證軟件、硬件、網絡和權限是否能夠協同工作。具有良好覆蓋率的健壯的端到端測試還可以幫助檢測基礎結構中的組件,使你能夠自信地進行基礎結構的更改。

慣用的 Rust 測試

Cargo 是一個包管理器,它封裝了一些其他標準工具,包括格式化器、篩選器,以及對我們來說最重要的測試工具。cargo test,我們可以使用 Rust 提供的內置註釋和支持來處理所有可能需要的測試,包括源文件中的單元測試和 tests 目錄中的集成測試。

編寫單元測試很簡單,在編寫應用程序代碼的文件中添加一個測試模塊,並添加一些 #[test] 註釋。這裏有一個例子:

#[cfg(test)]
mod tests {
  #[test]
  fn run_test() {
    // foo
  }
}

由於有 #[cfg(test)] 註釋,編譯器知道在實際構建中不包含此代碼。只有在執行 cargo test 時才編譯並運行它。

對於集成測試,我們只想測試 API 中公開的部分,因此按照慣例,這些測試將放在單獨的測試目錄中,在該目錄中它們不能訪問任何私有代碼。與單元測試類似,這些測試需要用 #[test] 註釋,並且只能由 cargo test 運行。

雖然我們的代碼應該總是有單元測試,但我們在本文中的主要關注點實際上是那些集成測試。如果你正在編寫一個 Rust 庫,打算供其他 Rust 代碼使用,那麼你可能需要測試一個公共 Rust API。但在我們的例子中,我們討論的是提供 web API 的服務。我們沒有 Rust API。相反,我們將通過網絡發出一些 HTTP 請求,它們可能是異步的。這改變了我們編寫這些測試的方法。

分別運行單元測試和端到端測試

運行端到端測試帶來的第一個變化是,你可能希望分別運行單元測試和集成測試。默認情況下,cargo test 將同時運行單元測試和集成測試。如果正在測試的代碼都在本地可用,這就不是問題。它之所以快是因爲沒有網絡延遲,而且還可以在最新的代碼上運行測試。但是,當在實際的非 prod 環境中運行測試時,測試可能需要更長的時間,並且需要將新代碼部署到該環境中,然後才能對其進行測試。

由於部署可能很長 (至少幾分鐘),你可能希望在部署之前運行單元測試。如果邏輯不正確,把代碼放在那裏是沒有意義的!因此,在運行任何端到端測試之前運行單元測試。然後,就可以自由部署了。只有在部署之後,才能運行端到端測試。

有兩種方法可以實現這種分離:

1,使用 Rust 內置的 #[ignore] 註釋

使用 #[ignore] 註釋是更簡單的方法,當你在 tests 目錄中編寫端到端測試並使用 #[test] 註釋它們時,繼續在下一行添加 #[ignore],如下所示:

#[test]
#[ignore]
fn run_test() {
  // foo
}

現在,當運行 cargo test 時,任何帶有 #[ignore] 的測試都將被跳過!如果我們正確地註釋了我們的測試,這意味着 cargo test 將只運行單元測試。然後,當要運行端到端測試時,可以執行 cargo test --ignored,它將只執行忽略的測試!

但是,如果想忽略其他測試,則此方法可能會失效。有些測試可能正在開發中,不希望它們作爲單元測試或集成測試步驟的一部分運行。在這種情況下,不能依靠 #[ignore] 來區分單元測試和集成測試。作爲一種替代方法,我更喜歡使用環境變量來控制測試何時運行。

2,使用環境變量來配置測試

使用環境變量配置測試可能很有用,原因有很多,包括控制測試的運行時間。對於配置變量,我喜歡用 “E2E_” 作爲前綴,以保持所有內容的組織性和可讀性。使用環境變量來啓用 / 禁用端到端測試,並在它們開始之前添加一些延遲,以便服務器有時間啓動。

由於需要在多個集成測試中重用配置,因此將配置代碼分解爲單獨的模塊。在集成測試目錄中設置模塊非常簡單:添加一個新目錄,並在該目錄中放置一個帶有代碼的 mod.rs 文件。如果模塊很複雜,可以在新目錄中放置多個文件,並在 mod.rs 文件中導出想要的任何代碼,就像處理普通源代碼一樣。

然後,需要在頂級集成測試文件中引用該模塊,就像在 main.rs 或 lib.rs 文件中聲明模塊一樣。

以下是實際應用程序中使用的一些測試配置代碼:

pub struct TestConfig {
    pub is_enabled: bool,
    pub delay_start_min: u64,
    pub env_name: String,
}

pub fn test_config() -> TestConfig {
    let is_enabled = env::var("E2E_ENABLE")
        .map(|s| &s.to_lowercase() == "true" || &s == "1")
        .unwrap_or(false);
    let delay_start_min = env::var("E2E_DELAY_START_MIN")
        .unwrap_or(String::from("0"))
        .parse::<u64>()
        .unwrap_or(0);
    let env_name = env::var("E2E_ENV_NAME").unwrap_or_else(|_| String::from("local"));

    TestConfig {
        is_enabled,
        delay_start_min,
        env_name,
    }
}

最後,如果 E2E_ENV_NAME 沒有設置 (例如,dev 或 staging),那麼它將默認爲 local,以防萬一針對 localhost 或類似的東西運行測試。然後可以在測試函數中引用這個 TestConfig 來跳過、延遲或生成特定於環境的模擬數據 (例如,獲取存在於暫存數據庫中的用戶 id)。

這裏的默認設置是,當不設置 E2E_ENABLE 的情況下運行 cargo 測試時,將只運行單元測試。然後,當準備好只運行集成測試時,可以將 E2E_ENABLE 設置爲 true 或 1,然後運行 cargo test。還可以通過傳入模塊名稱,篩選到想要的測試。如果將測試分組爲內聚單元,然後將這些單元放入它們自己的文件中,這將非常容易。

對測試進行分組並將組分開是很簡單的。默認情況下,tests 目錄頂層的每個文件都被編譯爲自己的 crate,因此每個文件都是獨立的,可以將相關的測試合併到這些文件中。

假設有一個 REST API,其中包含兩個不同的資源 apple 和 orange。可以將測試代碼分別寫入到兩個文件:apple_tests.rs 和 orange_tests.rs 中,每個測試文件都可以依賴於 test_config 模塊,並使用公共 test_config() 函數拉入測試配置。最重要的是,可以在運行 cargo test 時使用這些文件名來篩選測試。可以這樣做:

cargo test apple_tests orange_tests

異步測試

最重要的是,我們需要使用 async 運行測試。許多用於發出 HTTP 請求的 Rust 庫都是異步的,所以需要在異步函數中運行它們。然而,Rust 測試在默認情況下不支持異步。我們需要引入另一個庫來實現這一點。

最著名的是使用 tokio 庫提供的 test 宏。這個宏設置了一個 Tokio 運行時來包裝測試,允許將測試函數異步化。它非常容易使用:只需將 #[tokio::test] 替換爲正在使用的 #[test] 註釋,就可以了!

一個類似的替代方法是使用 actix_rt 庫,如果已經在使用 actix-web 來編寫的 web API,這個方法會更簡單。這個 crate 被建議用於爲 actix-web 的 web API 代碼運行異步單元測試,這意味着如果項目正在使用 actix-web,只需要在測試中使用 #[actix_rt::test] 註釋即可!

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