env_wrapper/lib.rs
1//! A wrapper around the standard [`std::env`](https://doc.rust-lang.org/std/env/index.html)
2//! functions that allows for a test double to be injected during testing.
3//!
4//! # Motivation
5//! Testing code that relies on the state of environment variables can be
6//! fragile, since the state may change between tests or be polluted by other tests.
7//! The ideal solution is to have a private set of environment variables per test,
8//! so these problems cannot happen.
9//!
10//! # Approach
11//! This crate introduces the [`RealEnvironment`](RealEnvironment)
12//! (a wrapper around the functions in [`std::env`](https://doc.rust-lang.org/std/env/index.html))
13//! and
14//! [`FakeEnvironment`](FakeEnvironment) structs, which implement the
15//! [`Environment`](Environment) trait. Instead of using
16//! [`std::env`](https://doc.rust-lang.org/std/env/index.html) directly,
17//! use [`RealEnvironment`](RealEnvironment) with
18//! [dependency injection](https://en.wikipedia.org/wiki/Dependency_injection)
19//! so each of your tests can have a private set of environment variables.
20//!
21//! # Example
22//! Scenario: An app looks for the presence of the `CONFIG_LOCATION` environment
23//! variable. If it isn't set, it uses a default location.
24//!
25//! ```rust
26//! use env_wrapper::{Environment, RealEnvironment};
27//!
28//! const CONFIG_LOCATION_ENV_VAR_NAME: &str = "CONFIG_LOCATION";
29//! const DEFAULT_CONFIG_LOCATION: &str = "/etc/my_app/service.conf";
30//!
31//! fn main() {
32//! // In the production code, inject RealEnvironment.
33//! let real_env = RealEnvironment;
34//! let config_location = get_config_location(real_env);
35//! }
36//!
37//! fn get_config_location(env: impl Environment) -> String {
38//! match env.var(CONFIG_LOCATION_ENV_VAR_NAME) {
39//! Ok(location) => location,
40//! _ => DEFAULT_CONFIG_LOCATION.to_string(),
41//! }
42//! }
43//!
44//! #[test]
45//! fn when_the_user_has_set_the_config_location_env_var_then_use_that_location() {
46//! use env_wrapper::FakeEnvironment;
47//!
48//! // Arrange
49//! // Each test should have a separate instance of FakeEnvironment.
50//! let mut fake_env = FakeEnvironment::new();
51//! let user_specified_location = "/a/user/specified/location";
52//! fake_env.set_var(CONFIG_LOCATION_ENV_VAR_NAME, user_specified_location);
53//!
54//! // Act
55//! // In the test code, inject FakeEnvironment.
56//! let location = get_config_location(fake_env);
57//!
58//! // Assert
59//! assert_eq!(location, user_specified_location);
60//! }
61//! ```
62
63#[cfg(test)]
64pub(crate) mod test_helpers;
65
66use std::{
67 collections::HashMap,
68 env::{self, VarError},
69 ffi::{OsStr, OsString},
70};
71
72/// Represents a process's environment.
73pub trait Environment {
74 /// Set an environment variable.
75 fn set_var(&mut self, key: impl AsRef<OsStr>, value: impl AsRef<OsStr>);
76
77 /// Get an environment variable, checking for valid UTF-8. If valid UTF-8
78 /// checks are not needed, use `var_os`.
79 ///
80 /// # Errors
81 /// * If a key doesn't exist, it should return a `VarError::NotPresent`.
82 /// * If the environment variable value contains invalid UTF-8, it
83 /// should return `VarError::NotUnicode(OsString)`.
84 fn var(&self, key: impl AsRef<OsStr>) -> Result<String, VarError>;
85
86 /// Get an environment variable. This does not check for valid UTF-8.
87 /// If a valid UTF-8 check is needed, use `var` instead.
88 fn var_os(&self, key: impl AsRef<OsStr>) -> Option<OsString>;
89
90 /// Remove an environment variable from the current process environment.
91 fn remove_var(&mut self, key: impl AsRef<OsStr>);
92}
93
94/// The process's environment. Wraps the standard
95/// [`std::env`](https://doc.rust-lang.org/std/env/index.html) functions.
96///
97/// When testing, [`FakeEnvironment`](FakeEnvironment) should likely be used instead.
98///
99/// # Note
100/// Different instances of the struct all reference the _same_ underlying process
101/// environment.
102///
103/// # Example
104/// ```rust
105/// # use env_wrapper::{Environment, RealEnvironment};
106/// let real_env = RealEnvironment;
107/// get_config_location(real_env);
108///
109/// fn get_config_location(env: impl Environment) {
110/// //...
111/// }
112/// ```
113pub struct RealEnvironment;
114
115impl Environment for RealEnvironment {
116 /// From [`std::env::set_var`](https://doc.rust-lang.org/std/env/fn.set_var.html):
117 /// > Sets the environment variable `key` to the value `value` for the currently running
118 /// > process.
119 /// >
120 /// > Note that while concurrent access to environment variables is safe in Rust,
121 /// > some platforms only expose inherently unsafe non-threadsafe APIs for
122 /// > inspecting the environment. As a result, extra care needs to be taken when
123 /// > auditing calls to unsafe external FFI functions to ensure that any external
124 /// > environment accesses are properly synchronized with accesses in Rust.
125 /// >
126 /// > Discussion of this unsafety on Unix may be found in:
127 /// >
128 /// > - [Austin Group Bugzilla](https://austingroupbugs.net/view.php?id=188)
129 /// > - [GNU C library Bugzilla](https://sourceware.org/bugzilla/show_bug.cgi?id=15607#c2)
130 /// >
131 /// > # Panics
132 /// >
133 /// > This function may panic if `key` is empty, contains an ASCII equals sign `'='`
134 /// > or the NUL character `'\0'`, or when `value` contains the NUL character.
135 fn set_var(&mut self, key: impl AsRef<OsStr>, value: impl AsRef<OsStr>) {
136 env::set_var(key, value)
137 }
138
139 /// From [`std::env::var`](https://doc.rust-lang.org/std/env/fn.var.html):
140 /// > Fetches the environment variable `key` from the current process.
141 /// >
142 /// > # Errors
143 /// >
144 /// > This function will return an error if the environment variable isn't set.
145 /// >
146 /// > This function may return an error if the environment variable's name contains
147 /// > the equal sign character (`=`) or the NUL character.
148 /// >
149 /// > This function will return an error if the environment variable's value is
150 /// > not valid Unicode. If this is not desired, consider using [`var_os`].
151 fn var(&self, key: impl AsRef<OsStr>) -> Result<String, VarError> {
152 env::var(key)
153 }
154
155 /// From [`std::env::var_os`](https://doc.rust-lang.org/std/env/fn.var_os.html):
156 /// > Fetches the environment variable `key` from the current process, returning
157 /// > [`None`] if the variable isn't set or there's another error.
158 /// >
159 /// > Note that the method will not check if the environment variable
160 /// > is valid Unicode. If you want to have an error on invalid UTF-8,
161 /// > use the [`var`] function instead.
162 /// >
163 /// > # Errors
164 /// >
165 /// > This function returns an error if the environment variable isn't set.
166 /// >
167 /// > This function may return an error if the environment variable's name contains
168 /// > the equal sign character (`=`) or the NUL character.
169 /// >
170 /// > This function may return an error if the environment variable's value contains
171 /// > the NUL character.
172 fn var_os(&self, key: impl AsRef<OsStr>) -> Option<OsString> {
173 env::var_os(key)
174 }
175
176 /// From [`std::env::remove_var`](https://doc.rust-lang.org/std/env/fn.remove_var.html):
177 /// > Removes an environment variable from the environment of the currently running process.
178 /// >
179 /// > Note that while concurrent access to environment variables is safe in Rust,
180 /// > some platforms only expose inherently unsafe non-threadsafe APIs for
181 /// > inspecting the environment. As a result extra care needs to be taken when
182 /// > auditing calls to unsafe external FFI functions to ensure that any external
183 /// > environment accesses are properly synchronized with accesses in Rust.
184 /// >
185 /// > Discussion of this unsafety on Unix may be found in:
186 /// >
187 /// > - [Austin Group Bugzilla](https://austingroupbugs.net/view.php?id=188)
188 /// > - [GNU C library Bugzilla](https://sourceware.org/bugzilla/show_bug.cgi?id=15607#c2)
189 /// >
190 /// > # Panics
191 /// >
192 /// > This function may panic if `key` is empty, contains an ASCII equals sign
193 /// > `'='` or the NUL character `'\0'`, or when the value contains the NUL
194 /// > character.
195 fn remove_var(&mut self, key: impl AsRef<OsStr>) {
196 env::remove_var(key)
197 }
198}
199
200/// A fake process environment, suitable for testing.
201///
202/// # Notes
203/// To make sure one test's environment state does not affect another, use a new
204/// instance of `FakeEnvironment` for each test.
205///
206/// # Example
207/// ```rust
208/// # use env_wrapper::{Environment, FakeEnvironment};
209/// const CONFIG_LOCATION_ENV_VAR_NAME: &str = "CONFIG_LOCATION";
210/// const DEFAULT_CONFIG_LOCATION: &str = "/etc/my_app/service.conf";
211///
212/// fn get_config_location(env: impl Environment) -> String {
213/// match env.var(CONFIG_LOCATION_ENV_VAR_NAME) {
214/// Ok(location) => location,
215/// _ => DEFAULT_CONFIG_LOCATION.to_string(),
216/// }
217/// }
218///
219/// #[test]
220/// fn when_the_user_has_set_the_config_location_env_var_then_use_that_location() {
221///
222/// // Arrange
223/// // Each test should have a separate instance of FakeEnvironment.
224/// let mut fake_env = FakeEnvironment::new();
225/// let user_specified_location = "/a/user/specified/location";
226/// fake_env.set_var(CONFIG_LOCATION_ENV_VAR_NAME, user_specified_location);
227///
228/// // Act
229/// // In test code, inject FakeEnvironment.
230/// let location = get_config_location(fake_env);
231///
232/// // Assert
233/// assert_eq!(location, user_specified_location);
234/// }
235/// ```
236#[derive(Clone, Debug, Default, Eq, PartialEq)]
237pub struct FakeEnvironment {
238 env_vars: HashMap<OsString, OsString>,
239}
240
241impl FakeEnvironment {
242 pub fn new() -> Self {
243 FakeEnvironment {
244 env_vars: HashMap::new(),
245 }
246 }
247}
248
249impl Environment for FakeEnvironment {
250 fn set_var(&mut self, key: impl AsRef<OsStr>, value: impl AsRef<OsStr>) {
251 self.env_vars
252 .insert(key.as_ref().into(), value.as_ref().into());
253 }
254
255 fn var(&self, key: impl AsRef<OsStr>) -> Result<String, VarError> {
256 match self.env_vars.get(key.as_ref()) {
257 Some(val) => match val.to_str() {
258 Some(valid_utf8) => Ok(valid_utf8.into()),
259 None => Err(VarError::NotUnicode(val.into())),
260 },
261 None => Err(VarError::NotPresent),
262 }
263 }
264
265 fn var_os(&self, key: impl AsRef<OsStr>) -> Option<OsString> {
266 self.env_vars.get(key.as_ref()).cloned()
267 }
268
269 fn remove_var(&mut self, key: impl AsRef<OsStr>) {
270 self.env_vars.remove(key.as_ref());
271 }
272}
273
274// These tests represent behavior that should be shared by fake and real
275// implementations. Both are being tested to enforce behavioral parity.
276#[cfg(test)]
277mod tests {
278 use std::{
279 env::VarError,
280 ffi::{OsStr, OsString},
281 os::unix::ffi::OsStrExt,
282 };
283
284 use crate::{test_helpers::random_upper, Environment, FakeEnvironment, RealEnvironment};
285
286 const INVALID_UTF8: [u8; 4] = [0x66, 0x6f, 0x80, 0x6f];
287
288 #[test]
289 fn when_adding_an_environment_variable_then_it_can_be_read() {
290 fn test(mut env: impl Environment) {
291 // Arrange
292 let key = random_upper();
293 let value = random_upper();
294 env.set_var(&key, &value);
295
296 // Act
297 let result = env.var(&key);
298
299 // Assert
300 assert_eq!(result.unwrap(), value);
301 }
302 test(RealEnvironment);
303 test(FakeEnvironment::new());
304 }
305
306 #[test]
307 fn given_a_nonexistent_env_var_when_getting_the_env_var_with_var_then_it_is_a_not_present_error(
308 ) {
309 fn test(env: impl Environment) {
310 // Arrange
311 let nonexistent_key = random_upper();
312
313 // Act
314 let result = env.var(nonexistent_key);
315
316 // Assert
317 assert_eq!(result.unwrap_err(), VarError::NotPresent);
318 }
319 test(RealEnvironment);
320 test(FakeEnvironment::new());
321 }
322
323 #[test]
324 fn when_setting_env_vars_then_multiple_data_types_can_be_used_on_the_same_environment_instance()
325 {
326 fn test(mut env: impl Environment) {
327 // Act
328 env.set_var(&random_upper(), &random_upper());
329 env.set_var(random_upper(), random_upper());
330 env.set_var(OsStr::new(&random_upper()), OsStr::new(&random_upper()));
331 env.set_var(
332 OsString::from(random_upper()),
333 OsString::from(random_upper()),
334 );
335
336 // Assert - none. This is strictly for type enforcement.
337 }
338 test(RealEnvironment);
339 test(FakeEnvironment::new());
340 }
341
342 #[test]
343 fn when_using_var_getter_with_an_invalid_utf8_value_then_it_is_a_not_unicode_error() {
344 fn test(mut env: impl Environment) {
345 // Arrange
346 let key = random_upper();
347 env.set_var(&key, OsStr::from_bytes(&INVALID_UTF8));
348
349 // Act
350 let result = env.var(&key);
351
352 // Assert
353 assert!(matches!(result, Err(VarError::NotUnicode(_))));
354 }
355 test(RealEnvironment);
356 test(FakeEnvironment::new());
357 }
358
359 #[test]
360 fn given_a_nonexistent_env_var_when_getting_the_env_var_with_var_os_then_it_is_none() {
361 fn test(env: impl Environment) {
362 // Arrange
363 let key = random_upper();
364
365 // Act
366 let result = env.var_os(key);
367
368 // Assert
369 assert!(result.is_none());
370 }
371 test(RealEnvironment);
372 test(FakeEnvironment::new());
373 }
374
375 #[test]
376 fn given_an_env_var_with_invalid_utf8_when_getting_the_env_var_with_var_os_then_it_is_some() {
377 fn test(mut env: impl Environment) {
378 // Arrange
379 let key = random_upper();
380 env.set_var(&key, OsStr::from_bytes(&INVALID_UTF8));
381
382 // Act
383 let result = env.var_os(&key);
384
385 // Assert
386 assert!(result.is_some());
387 }
388 test(RealEnvironment);
389 test(FakeEnvironment::new());
390 }
391
392 #[test]
393 fn given_an_existing_environment_variable_when_setting_the_same_environment_variable_then_the_value_is_overwritten(
394 ) {
395 fn test(mut env: impl Environment) {
396 // Arrange
397 let key = random_upper();
398 let val_1 = random_upper();
399 let val_2 = random_upper();
400 env.set_var(&key, &val_1);
401
402 // Act
403 env.set_var(&key, &val_2);
404
405 // Assert
406 assert_eq!(env.var(&key).unwrap(), val_2);
407 }
408 test(RealEnvironment);
409 test(FakeEnvironment::new());
410 }
411
412 #[test]
413 fn given_an_existing_environment_variable_when_removing_the_same_environment_variable_then_the_variable_no_longer_exists(
414 ) {
415 fn test(mut env: impl Environment) {
416 // Arrange
417 let key = random_upper();
418 let value = random_upper();
419 env.set_var(&key, &value);
420
421 // Act
422 env.remove_var(&key);
423
424 // Assert
425 assert_eq!(env.var(&key).unwrap_err(), VarError::NotPresent);
426 }
427 test(RealEnvironment);
428 test(FakeEnvironment::new());
429 }
430
431 #[test]
432 fn when_removing_a_nonexistent_environment_variable_then_do_not_panic() {
433 fn test(mut env: impl Environment) {
434 // Arrange
435 let key = random_upper();
436
437 // Act
438 env.remove_var(&key);
439
440 // Assert - no assertion
441 }
442
443 test(RealEnvironment);
444 test(FakeEnvironment::new());
445 }
446}