What is the best way to unit test network functions in rust?


#1

Suppose I have a function which on some condition based on the passed arguments performs a network request and/or not. In my current implementation I use reqwest crate if it is necessary, however, I have not ever tested the network functions ever. Well, not ever, I did that in Java, I simply created a base interface for my network requester and implemented both production requester and test one, so that was quite easy. I knew about google gmock but this is obviously not suitable here. So, how do you do that?

The example code:

/// Gets the steam ID 64 string from steam id. If the steam id is already in this format - it is returned.
/// [Reference example](http://steamid.co/php/api.php?action=steamIDTO64&id=STEAM_0:1:123456)
pub fn get_steamid64_from_string(string: &str) -> Result<String> {
    use regex::Regex;

    if string.parse::<u64>().is_ok() {
        // The passed string is already a steam id 64 string.
        return Ok(string.to_owned());
    }

    #[derive(Deserialize)]
    struct Response {
        #[serde(rename = "steamID64")]
        steam_id_64: String,
    }

    if Regex::new(r#"^STEAM_0:\d:\d+$"#)?.is_match(string) {
        debug!("Provided steam id is in SteamID format: {}", string);
        let url = &format!(
            "http://steamid.co/php/api.php?action=steamIDTO64&id={}",
            string
        );
        Ok(serde_json::from_str::<Response>(&get(url)?)?.steam_id_64)
    } else {
        bail!("Invalid steam id")
    }
}

I am interested in general solution, not specific to reqwest or hyper or anything else. I need the idea.
The only one thing that comes to my mind is to create your own network-request trait, implement it for your network requester and so create a fake test network requester.


#2

I usually do exactly what you already suggested: I abstract away the implementation with a trait and use a test double that provides predetermined values. One problem with this approach that complicated lifetime relationships cannot be expressed this way. For example, suppose the network requester provides you a request token that cannot outlive the requester itself:

struct NetworkRequester {
    // fields omitted
}

impl NetworkRequester {
    fn make_request<'a>(&'a self, request: Request) -> RequestToken<'a> {
        // details omitted
    }
}

You put NetworkRequester and RequestToken behind a trait, because it’s currently impossible to specify RequestToken as an associated type because it has a lifetime parameter. I currently have to work around this in my code, but it should be resolved this year.


#3

Looks like there is no any more beatiful solution :slight_smile: Thank you for your response.