iT邦幫忙

2023 iThome 鐵人賽

DAY 10
0
SideProject30

30 天用 Rust 打造 QR Code 製造機系列 第 10

Day 10 - 為 Rust 單元測試

  • 分享至 

  • xImage
  •  

今天是第 10 天,我們的功能已經做到一個段落,接下來需要做一個滿重要的部分,那就是測試。雖然我們都有在 Postman 測試功能是否可以執行,不過我們還是必須為這個專案加上測試,以確保其穩定性和可靠性。

為什麼需要測試?

在任何軟體開發專案中,測試都是非常重要的一環。測試不僅能確保你的程式碼功能正確,還能在未來的開發過程中,作為一個保險機制,確保新加入的功能或者修改不會影響到現有的功能。

測試也可以讓我們找出是否有哪些潛在的問題,並且提前找出來去解決,也可以讓我們對自己的開發更有信心。

單元測試與整合測試

在 Rust 中,有兩種主要的測試類型:

  • 單元測試(Unit Tests):這些測試針對單一函式或模組進行,確保它們按照預期運作。
  • 整合測試(Integration Tests):這些測試則檢查多個模組或整個應用是否能正確運作。

今天主要會做單元測試的部分。

進行的測試

主要會從 API 的部分,並且針對三個功能進行測試:

  • is_valid_color:檢查顏色的色碼是否正確符合規定。
  • get_coordinates:從地址獲取經緯度。
  • get_code_data:根據提供的資料產生 QR code。

測試 is_valid_color

這個函式的主要目的是確保傳入的顏色碼是一個有效的 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

測試 get_coordinates

由於這個函式的功能會從 Geocoding API 去做一個 HTTP 請求的動作,所以我們需要先安裝套件來幫我們的測試做處理,分別是:

  • mockito
  • actix_rt

先從 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_code_data

最後這個的測試相對複雜一些,因為它依賴於 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,測試通過感覺心裡也踏實許多。我們明天繼續未完的測試!


上一篇
Day 9 - 產生地址和 Mail 的 QR code
下一篇
Day 11 - 為 Rust 整合測試
系列文
30 天用 Rust 打造 QR Code 製造機30
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言