Complexity
Complexity

Reputation: 5820

Abstract OS environment variables for testing

I have created a Server struct which consists out of 2 ports, an IP address and a port. The value assigned to the port could NOT be specified, but it's read from the system's environment variables. When the environment variable has not been found, or when it's not a valid value, it defaults to a standard port.

struct Server {
    ip: String,
    port: u16,
}

impl Server {
    const STD_PORT: u16 = 4333;
    const PORT_ENV_VAR: &'static str = "MEMDB_PORT";

    fn new(ip: String) -> Server {
        Server {
            ip: ip,
            port: Server::get_port(),
        }
    }

    fn get_port() -> u16 {
        match env::var(Server::PORT_ENV_VAR) {
            Ok(val) => match val.parse::<u16>() {
                Ok(val) => val,
                Err(_) => Server::STD_PORT,
            },
            Err(_) => Server::STD_PORT,
        }
    }
}

When I want to create tests for it, it can be done in the following way:

#[cfg(test)]
mod tests {
    use crate::Server;

    #[test]
    fn test_create_server_assigns_correct_ip() {
        // WHEN:
        let server = crate::Server::new("127.0.0.1".to_owned());

        // THEN:
        assert_eq!(server.ip, "127.0.0.1");
    }

    #[test]
    fn test_create_server_assigns_correct_port() {
        // WHEN:
        let server = crate::Server::new("".to_owned());

        // THEN:
        assert_eq!(server.port, 4333);
    }
}

Off course, this test produces a different result when the environment variable MEMDB_PORT has a value assigned.

How would I be able to verify that the assigned port can be controlled by setting the correct environment variable?

I was thinking about creating a trait that defined the API to access the system's environment variables.

trait EnvironmentReader {
    fn get(key: &str) -> Option<String>;
}

struct OSEnvironmentReader {}

impl EnvironmentReader for OSEnvironmentReader {
    fn get(key: &str) -> Option<String> {
        return match env::var(key) {
            Ok(val) => Some(val),
            Err(_) => None,
        };
    }
}

Is this the correct way on how to do it in Rust? If so, how should I modify my struct to use this trait? Add it as a field to the struct itself, and how can I do this with the new 'dyn' stuff?

Upvotes: 2

Views: 938

Answers (1)

Thomas
Thomas

Reputation: 181785

There is no need for dyn or traits here. Simply create a configuration struct:

pub struct ServerConfig {
    pub port: u16,
}

impl ServerConfig {
    const STD_PORT: u16 = 4333;
    const PORT_ENV_VAR: &'static str = "MEMDB_PORT";

    fn from_environment() -> ServerConfig {
        ServerConfig {
            port: Self::port_from_env(),
        }
    }

    fn port_from_env() -> u16 {
        // Also shortened the `match` a bit here. Could make this generic too.
        env::var(ServerConfig::PORT_ENV_VAR)
            .map(|val| val.parse::<u16>())
            .unwrap_or(ServerConfig::STD_PORT)
    }
}

Then pass it into your Server when constructing it:

struct Server {
    config: Config,
    ip: String,
}

impl Server {
    fn new(config: Config, ip: String) -> Server {
        Server {
            config,
            ip,
        }
    }
}

In production code, pass ServerConfig::from_environment(). In your test code, pass a custom ServerConfig { port: 4333 }. You're still not testing whether the port is correctly taken from the environment, but in the comments you mentioned that you didn't want that and would rather mock it out.

You could also move ip into ServerConfig if it fits better with your design.

Upvotes: 1

Related Questions