【问题标题】:Abstract OS environment variables for testing用于测试的抽象 OS 环境变量
【发布时间】:2021-01-29 13:41:59
【问题描述】:

我创建了一个 Server 结构,它由 2 个端口、一个 IP 地址和一个端口组成。 无法指定分配给端口的值,但它是从系统的环境变量中读取的。当没有找到环境变量,或者它不是一个有效值时,它默认为标准端口。

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,
        }
    }
}

当我想为它创建测试时,可以通过以下方式完成:

#[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);
    }
}

当然,当环境变量 MEMDB_PORT 被赋值时,这个测试会产生不同的结果。

如何验证分配的端口是否可以通过设置正确的环境变量来控制?

我正在考虑创建一个特征来定义 API 以访问系统的环境变量。

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,
        };
    }
}

这是如何在 Rust 中做到这一点的正确方法吗? 如果是这样,我应该如何修改我的结构来使用这个特性?将其作为字段添加到结构本身,我该如何使用新的“dyn”内容?

【问题讨论】:

  • 您可以在测试中使用 std::env::set_var 来设置测试期间的环境变量。
  • 这不是最佳解决方案。如果我改用文件系统,您可能会争辩说我可以在测试中创建临时文件来进行测试,但这会大大降低测试速度。我真的需要一种方法来模拟这些依赖关系,只是寻找如何做到这一点的“Rust”方式。
  • 在这种情况下,您可能需要一些通用的配置处理程序特征,您可以将其作为 Box&lt;dyn Config&gt; 注入您的服务器并交换不同的实现以进行测试。
  • 这就是我遇到的一些问题。我有一个特点(看我的代码,但不知道如何正确使用它,盒子的东西......)

标签: rust


【解决方案1】:

这里不需要dyn 或特征。只需创建一个配置结构:

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)
    }
}

然后在构造它时将它传递给您的Server

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

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

在生产代码中,传递ServerConfig::from_environment()。在您的测试代码中,传递一个自定义的ServerConfig { port: 4333 }。您仍然没有测试该端口是否正确地从环境中获取,但是在您提到的 cmets 中您不想要它并且宁愿将其模拟出来。

如果ip 更适合您的设计,您也可以将其移至ServerConfig

【讨论】:

  • 谢谢。塞曼说我确实把事情复杂化了....
  • 我在这里看到一个问题,那就是当环境变量中的值不是有效的 u16 时,我无法测试是否返回默认端口。使用特征来覆盖这种情况是否有益?如果是这样,你能更新你的答案吗?
  • 您可以将函数 (FnOnce) 传递给 from_environment。在生产中,传递std::env::var,在测试中,传递一个返回固定值的lambda。除了函数,您还可以使用特征,例如fn from_environment&lt;G: VarGetter&gt;(var_getter: V)trait VarGetter { fn get_var(&amp;self, name: &amp;str) -&gt; Result&lt;String, Error&gt;; } 或类似的东西。一个生产实现只调用std::env::var,一个模拟测试返回任何东西。
猜你喜欢
  • 1970-01-01
  • 2014-09-01
  • 2023-02-02
  • 2011-08-30
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多