在這部分最一開始提到很多我們在 Python 中熟悉的概念都能轉換到 Rust 中,前幾天建立的 GitHub Template 便是其中一個例子。
而其中雖然有使用到測試的部分,但都沒有詳細說明,今天我們就要來說說怎麼寫啦!
程式碼測試有許多好處,其中一個好處就是幫助我們發現程式碼中的錯誤、缺陷和漏洞,提高軟體的可靠性。
簡單來說,就是幫忙抓蟲,所以 Ferris 也來幫忙抓王蟲啦!!!
🚨 測試是很複雜的技能,我不認為有辦法在短時間就涵蓋如何寫出好測試的細節,但我們還是會討論 Rust 測試的功能機制,以及在 MLOps 情境中可以做的一些測試。
在談如何撰寫測試之前,我們首先從重構 cargo new
生成的初始資料夾結構說起。
在專案一開始被生成的時後,主要程式碼都會放在 src/main.rs
中的 main
函式中。
但隨著程式增長,會發現 main
函式負責太多事情了,而一般來說只有在每個函式都只負責一件事時,函式才能簡潔且易於維護。
事實上,因為將多個任務的責任分配給 main
函式的組織問題在許多執行檔專案中都很常見。
所以 Rust 社群已經制定了在 main
函式開始變大時,如何拆分任務的指導原則:
main.rs
與 lib.rs
並將程式邏輯放到 lib.rs
。main.rs
。main.rs
移至 lib.rs
。這個模式可以將任務拆分為:
main.rs
處理程式的執行。lib.rs
處理眼前的所有任務邏輯。由於我們無法直接測試 main
,這個架構讓我們可以測試所有移至 lib.rs
的程式函式邏輯。
而留在 main.rs
的程式碼會非常小,所以直接用看的就能驗證。
而我們還能進一步將測試的程式碼放在另一個資料夾中,範例資料夾結構如下:
.
├── Cargo.toml
├── src
│ ├── lib.rs
│ └── main.rs
└── tests
└── test_lib.rs
事實上,在開發程式時盡早重構是很好的做法,因為重構少量的程式碼會比較簡單。
例如這裡只是建立幾個檔案而已,但我們已經為未來維護程式碼打下良好的基礎。
在 Rust 中,測試區塊的基礎模板如下:
#[cfg(debug)]
mod tests {
#[test]
fn test_1 {
assert_eq!(...)
}
}
其中借助了模組化 (mod
) 並引入 #[cfg(debug)]
與 #[test]
兩個巨集。
而測試的本身則借助了 assert_eq 巨集 (若程式中斷則判斷為測試失敗)。
除了 assert_eq
以外,還有 assert 與 assert_ne 可以使用。
以下為實際範例 (為了方便說明,這裡先把測試和函式放在同一個檔案中):
fn all_caps(word: &str) -> String {
word.to_uppercase()
}
fn main() {}
#[cfg(test)]
mod tests {
use crate::*;
#[test]
fn check_all_caps() {
let result = all_caps("hello");
let expected = String::from("HELLO");
assert_eq!(result, expected, "string should be all uppercase")
}
}
而要執行測試則可以使用前幾天就看過的 cargo test
指令。
可以發現,整體流程跟在 Python 中使用 pytest 幾乎一模一樣,不需要重新學一個技能!
💡 一個 crate 是 Rust 編譯器同個時間內視為程式碼的最小單位。
當我們傳入單一源碼檔案至cargo
(或rustc
) 時,編譯器會將該檔案視為一個 crate。
因此use crate::*;
中的 crate 指的是檔案內所有程式碼,而這麼寫代表在此模組內引入所有模組外的東西。
測測試驅動開發(Test-Driven Development,TDD)是一種軟體開發方法,它的核心理念是在撰寫實際的程式碼之前先撰寫測試用例。
TDD 的過程通常分為以下三個主要步驟:
更詳盡的流程如下圖:
TDD 的主要優點包括:
然而,TDD 也需要更多的時間和努力,因為需要撰寫大量的測試用例。此外,TDD 不一定適用於所有類型的項目,並且在某些情況下,可能需要靈活適應開發流程。儘管如此,它仍然是一種有價值的開發方法,可以提高軟體品質並減少後期的問題。
除了常見的測試情境以外,其實在 MLOps 的許多環節也可以加上測試,明天我們就來看看有哪些可能性吧!