今天是第 10 天,我們的功能已經做到一個段落,接下來需要做一個滿重要的部分,那就是測試。雖然我們都有在 Postman 測試功能是否可以執行,不過我們還是必須為這個專案加上測試,以確保其穩定性和可靠性。
在任何軟體開發專案中,測試都是非常重要的一環。測試不僅能確保你的程式碼功能正確,還能在未來的開發過程中,作為一個保險機制,確保新加入的功能或者修改不會影響到現有的功能。
測試也可以讓我們找出是否有哪些潛在的問題,並且提前找出來去解決,也可以讓我們對自己的開發更有信心。
在 Rust 中,有兩種主要的測試類型:
今天主要會做單元測試的部分。
主要會從 API 的部分,並且針對三個功能進行測試:
這個函式的主要目的是確保傳入的顏色碼是一個有效的 16 進制色碼。在測試中,我們要確定可以正確地識別正確與否的色碼。
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_is_valid_color() {
assert!(is_valid_color("#FFFFFF"));
assert!(is_valid_color("#000000"));
assert!(!is_valid_color("#GGGGGG"));
assert!(!is_valid_color("FFFFFF"));
}
}
在 src/api/mod.rs
的最下面加上這一段測試後,給予正確與不正確的色碼去做測試,並且在 Terminal 執行 cargo test
,就可以讓 Cargo 幫我們的程式碼做這個測試。
測試成功就會回傳以下結果:
❯ cargo test
Compiling qrcode-actix v0.1.0 (/Users/buckychu/sideProjects/qrcode-actix)
Finished test [unoptimized + debuginfo] target(s) in 1.68s
Running unittests src/main.rs (target/debug/deps/qrcode_actix-f5c31d729f1fd710)
running 1 test
test api::tests::test_is_valid_color ... ok
test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s
由於這個函式的功能會從 Geocoding API 去做一個 HTTP 請求的動作,所以我們需要先安裝套件來幫我們的測試做處理,分別是:
先從 Terminal 執行指令安裝:
cargo add mockito actix_rt
接下來在測試區塊引入 mockito,並新增測試:
#[cfg(test)]
mod tests {
use super::*;
use mockito::Server;
// 省略
#[actix_rt::test]
async fn test_get_coordinates() {
let api_key = match env::var("GOOGLE_MAPS_API_KEY") {
Ok(key) => key,
Err(_) => {
println!("Warning: 讀取不到 GOOGLE_MAPS_API_KEY,跳出測試");
return;
}
};
let mut server = Server::new();
let mock = server
.mock(
"GET",
format!(
"https://maps.googleapis.com/maps/api/geocode/json?address=test&key={}",
api_key
)
.as_str(),
)
.with_status(200)
.with_body(
r#"{
"results": [
{
"geometry": {
"location": {
"lat": 40.0,
"lng": -100.0
}
}
}
]
}"#,
)
.create();
let result = get_coordinates("test").await;
match result {
Ok((lat, lng)) => {
assert_eq!(lat, 40.0);
assert_eq!(lng, -100.0);
}
Err(e) => {
println!("Debug: Error returned: {:?}", e);
panic!("get_coordinates returned an error");
}
}
mock.assert();
}
}
接下來在測試的過程中,發現 get_coordinates()
有錯誤,所以還好有測試幫我們發現問題,並順便修正:
#[derive(Debug)]
enum MyError {
MissingField(String),
ReqwestError(reqwest::Error),
}
impl From<reqwest::Error> for MyError {
fn from(err: reqwest::Error) -> MyError {
MyError::ReqwestError(err)
}
}
async fn get_coordinates(address: &str) -> Result<(f64, f64), MyError> {
dotenv().ok();
let api_key = env::var("GOOGLE_MAPS_API_KEY").expect("GOOGLE_MAPS_API_KEY must be set");
let url = format!(
"https://maps.googleapis.com/maps/api/geocode/json?address={}&key={}",
address, api_key
);
let response: serde_json::Value = reqwest::get(&url).await?.json().await?;
let lat = response["results"][0]["geometry"]["location"]["lat"]
.as_f64()
.ok_or(MyError::MissingField(
"Latitude is missing or not a float".to_string(),
))?;
let lng = response["results"][0]["geometry"]["location"]["lng"]
.as_f64()
.ok_or(MyError::MissingField(
"Longitude is missing or not a float".to_string(),
))?;
Ok((lat, lng))
}
再執行 cargo test
,這個測試也一樣沒問題了。
最後這個的測試相對複雜一些,因為它依賴於 get_coordinates()
。但在這個測試中,我們主要關注的是能否根據不同的 Info
結構體去產生相對應的 QR code。
impl Default for Info {
fn default() -> Self {
Self {
url: None,
phone: None,
email: None,
address: None,
background: None,
dimension: None,
foreground: None,
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use mockito::Server;
// 省略
#[actix_rt::test]
async fn test_get_code_data() {
let mut info = Info::default();
info.url = Some("https://example.com".to_string());
let result = get_code_data(&info).await;
assert_eq!(result, Some("https://example.com".as_bytes().to_vec()));
info.url = None;
info.phone = Some("1234567890".to_string());
let result = get_code_data(&info).await;
assert_eq!(result, Some("tel:1234567890".as_bytes().to_vec()));
info.phone = None;
info.email = Some("test@example.com".to_string());
let result = get_code_data(&info).await;
assert_eq!(result, Some("mailto:test@example.com".as_bytes().to_vec()));
info.email = None;
info.address = Some("277 Bedford Avenue, Brooklyn, NY 11211, USA".to_string());
info.address = None;
let result = get_code_data(&info).await;
assert_eq!(result, None);
}
}
最後執行 cargo test
的結果:
❯ cargo test
Compiling qrcode-actix v0.1.0 (/Users/buckychu/sideProjects/qrcode-actix)
Finished test [unoptimized + debuginfo] target(s) in 1.84s
Running unittests src/main.rs (target/debug/deps/qrcode_actix-f5c31d729f1fd710)
running 3 tests
test api::tests::test_get_code_data ... ok
test api::tests::test_get_coordinates ... ok
test api::tests::test_is_valid_color ... ok
test result: ok. 3 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.01s
以上是單元測試的結果,解決了潛在的 bug,測試通過感覺心裡也踏實許多。我們明天繼續未完的測試!