Purpose of This Chapter
Ensuring the correctness and reliability of our password generator is paramount. Unit tests allow us to verify that individual components of our application work as expected. In this chapter, we will write basic unit tests for our build_char_pool function and the generate_single_password function to catch regressions and validate our logic.
Concepts Explained
Unit Testing: Testing individual units or components of your code (e.g., functions, methods) in isolation to ensure they behave correctly.
Cargo Test: Rust’s built-in testing framework. You write tests directly in your Rust source files (often in a tests module) and run them using cargo test.
Assertions: Functions or macros (like assert!, assert_eq!, assert_ne!) used within tests to check if a condition is true, if two values are equal, etc. If an assertion fails, the test fails.
#[cfg(test)] and mod tests: Conventionally, tests in Rust are placed in a mod tests block annotated with #[cfg(test)]. This attribute tells Rust to compile and run this module only when running tests, not when building the release binary.
Test Functions: Functions annotated with #[test] are recognized by Cargo as test cases.
Step-by-Step Tasks
1. Add Test Module and First Test
Open rpassword-gen/src/main.rs. At the bottom of the file, add a tests module.
// ... (existing code: imports, constants, PasswordGenError, Args struct,
// build_char_pool function, generate_single_password function, run function)
fn main() {
env_logger::init();
info!("Logger initialized.");
if let Err(e) = run() {
error!("Application encountered an error: {}", e);
process::exit(1);
}
info!("Application finished successfully.");
}
#[cfg(test)] // This module is only compiled when running tests
mod tests {
use super::*; // Import everything from the outer scope
#[test]
fn test_build_char_pool_all_defaults() {
let args = Args {
length: 16,
count: 1,
uppercase: false,
lowercase: false,
numbers: false,
symbols: false,
};
let pool = build_char_pool(&args);
// Assert that the pool contains characters from all sets when no flags are set
assert!(pool.contains(LOWERCASE_CHARS));
assert!(pool.contains(UPPERCASE_CHARS));
assert!(pool.contains(NUMERIC_CHARS));
assert!(pool.contains(SYMBOL_CHARS));
// Check for expected total length of default combined character set
assert_eq!(
pool.len(),
LOWERCASE_CHARS.len() + UPPERCASE_CHARS.len() + NUMERIC_CHARS.len() + SYMBOL_CHARS.len()
);
}
}
Explanation of the new test code:
#[cfg(test)]: Ensures this module is only included during testing.mod tests { ... }: Declares a module namedtests.use super::*;: Brings all items from the parent scope (ourmain.rsfile) into this test module, allowing us to accessArgs,build_char_pool,LOWERCASE_CHARS, etc.#[test]: Markstest_build_char_pool_all_defaultsas a test function.let args = Args { ... };: We manually create anArgsstruct with specific values to simulate command-line input.let pool = build_char_pool(&args);: Call the function we want to test.assert!(pool.contains(LOWERCASE_CHARS));: Checks if the generatedpoolstring contains all characters from ourLOWERCASE_CHARSconstant. This validates that the correct character sets are included.assert_eq!(pool.len(), ...);: Verifies that the length of the combined pool is correct.
2. Add More Tests
Let’s add a few more tests for different scenarios: specific character sets and password generation.
// ... (previous test_build_char_pool_all_defaults test)
#[test]
fn test_build_char_pool_specific_types() {
let args = Args {
length: 16,
count: 1,
uppercase: true,
lowercase: false,
numbers: true,
symbols: false,
};
let pool = build_char_pool(&args);
assert!(pool.contains(UPPERCASE_CHARS));
assert!(!pool.contains(LOWERCASE_CHARS)); // Ensure not included
assert!(pool.contains(NUMERIC_CHARS));
assert!(!pool.contains(SYMBOL_CHARS)); // Ensure not included
assert_eq!(
pool.len(),
UPPERCASE_CHARS.len() + NUMERIC_CHARS.len()
);
}
#[test]
fn test_generate_single_password_length() {
let args = Args {
length: 10,
count: 1,
uppercase: true,
lowercase: true,
numbers: true,
symbols: true,
};
let pool = build_char_pool(&args);
let pool_chars: Vec<char> = pool.chars().collect();
// Need to pass a slice of chars, not String directly
let password = generate_single_password(&args, &pool_chars);
assert_eq!(password.len(), 10); // Ensure correct length
}
#[test]
fn test_generate_single_password_valid_chars() {
let args = Args {
length: 20,
count: 1,
uppercase: true,
lowercase: true,
numbers: false,
symbols: false,
};
let pool = build_char_pool(&args);
let pool_chars: Vec<char> = pool.chars().collect();
let allowed_chars: Vec<char> = pool.chars().collect();
let password = generate_single_password(&args, &pool_chars);
assert_eq!(password.len(), 20);
// Check if every character in the generated password is within the allowed pool
for char_in_pwd in password.chars() {
assert!(allowed_chars.contains(&char_in_pwd), "Password contains disallowed character: {}", char_in_pwd);
}
}
} // End of mod tests
3. Run Your Tests
Save src/main.rs and run all tests using Cargo:
cargo test
Expected output:
Compiling rpassword-gen v0.1.0 (/path/to/rpassword-gen)
Finished test [unoptimized + debuginfo] target(s) in X.XXs
Running unittests (target/debug/deps/rpassword_gen-XXXXXXXXXXX)
running 4 tests
test tests::test_build_char_pool_all_defaults ... ok
test tests::test_build_char_pool_specific_types ... ok
test tests::test_generate_single_password_length ... ok
test tests::test_generate_single_password_valid_chars ... ok
test result: ok. 4 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s
If all tests pass, you’ll see ok for each test and a summary showing 4 passed. If any fail, Cargo will provide detailed information about the failure.
Tips/Challenges/Errors
- Test Data: When writing unit tests, create specific
Argsinstances (or whatever input your function takes) that represent various scenarios (defaults, specific flags, edge cases). - Test Isolation: Unit tests should ideally be isolated, meaning they don’t depend on each other or external factors like network access or file system. Our tests here for
build_char_poolandgenerate_single_passwordare pure functions, making them easy to test. - Randomness in Tests: Testing random output can be tricky.
- For password length, it’s deterministic (
assert_eq!(password.len(), ...)). - For character types, we assert that the characters used are from the allowed set. We don’t try to predict the exact random sequence.
- For password length, it’s deterministic (
- Integration Tests: For testing how your entire CLI tool works from the command line (e.g., parsing arguments and outputting the final password), you would typically use integration tests in a separate
tests/directory (notsrc/main.rs). We are focusing on unit tests here for core logic.
Summary/Key Takeaways
In this chapter, you successfully:
- Learned how to create a
#[cfg(test)]module for unit tests. - Wrote unit tests for the
build_char_poolfunction, verifying both default and specific character set inclusions. - Created unit tests for
generate_single_passwordto confirm correct length and character validity. - Ran all your tests using
cargo testand ensured they pass.
By adding unit tests, we’ve significantly improved the confidence in our application’s core logic, making it more resilient to future changes. In the final chapter, we’ll cover how to deploy our Rust CLI application for general use.