Rust
Completed
-
Section 1: Getting Started
-
Section 2: Core Syntax
-
Section 3: Ownership and Borrowing
-
Section 4: Structuring Data
-
Section 5: Collections and Error Handling
-
Section 6: Traits and Generics
-
Section 7: Concurrency
-
Section 8: Building for the Web
-
Section 9: Memory and Performance
-
Section 10: More Standard Library and Ecosystem
-
Section 11: Advanced Rust
-
Section 12: Rust for Systems and WebAssembly
-
Section 13: Tooling and Best Practices
-
Section 14: Data Structures and Algorithms in Rust
-
Section 15: Practical Projects
-
Section 16: Interview Practice
-
Section 17: std::collections In Depth
-
Section 18: std::io and std::fs In Depth
-
Section 19: std::net
-
Section 20: std::option and std::result In Depth
-
Section 21: std::iter In Depth
-
Section 22: std::sync In Depth
-
Section 23: std::string and std::str
-
Section 24: Cargo and Crates.io Ecosystem
-
Section 25: Popular Crates Ecosystem
-
Section 26: Rust Memory Model Deep Dive
-
Section 27: More Practice Exercises
-
Section 28: More Interview Practice
-
Section 29: Async Rust Deep Dive
-
Section 30: Tokio Ecosystem In Depth
-
Section 31: Error Handling Ecosystem Deep Dive
-
Section 32: Serde In Depth
-
Section 33: Web Development with Rust Deep Dive
-
Section 34: Database Access Ecosystem
-
Section 35: Rust for Embedded Systems Deep Dive
-
Section 36: Rust Macros In Depth
-
Section 37: Advanced Trait System
-
Section 38: Unsafe Rust In Depth
-
Section 39: Rust CLI Development
-
Section 40: Testing Ecosystem Deep Dive
-
Section 41: WebAssembly Deep Dive
-
Section 42: Rust Design Patterns
-
Section 43: More Data Structures in Rust
-
Section 44: Final Practice Projects
-
Section 45: Rust Performance Optimization
-
Section 46: Rust Ecosystem Tooling
-
Section 47: More Interview and Review
207: Table-Driven Tests in Rust
You've probably already written a few dozen tests in Rust by now. Usually, it looks like this: write a function, write a #[test], and call assert_eq!. That works great for one or two cases. But what happens when you have twenty different edge cases to cover? Writing twenty separate test functions is tedious, and writing one giant test function with twenty assert_eq! calls is a nightmare because the moment the third one fails, the whole test stops, and you have no idea how the other seventeen fared.
That's where table-driven tests come in. Instead of writing the logic of the test over and over, we separate the data (the inputs and expected outputs) from the execution (the loop that runs the test).
Building a simple slugifier
Let's build a small utility that converts a string into a URL-friendly "slug." It should lowercase everything, replace spaces with hyphens, and strip out non-alphanumeric characters. Here is the basic implementation I'm starting with:
fn slugify(text: &str) -> String {
text.to_lowercase()
.chars()
.filter(|c| c.is_alphanumeric() || *c == ' ')
.map(|c| if c == ' ' { '-' } else { c })
.collect()
}
The repetitive way to test
If I were being lazy, I might just write a few tests like this:
#[test]
fn test_slugify_basic() {
assert_eq!(slugify("Hello World"), "hello-world");
}
#[test]
fn test_slugify_special_chars() {
assert_eq!(slugify("Rust & Coffee!"), "rust--coffee");
}
#[test]
fn test_slugify_empty() {
assert_eq!(slugify(""), "");
}
This is fine for now, but it's boilerplate-heavy. Every time I think of a new edge case—like leading spaces or emojis—I have to write a whole new function. I'd rather just add a new row to a list.
Moving the data into a table
I'll create a slice of tuples. Each tuple will contain the input string and the expected result. Then, I'll just loop through them. This is the essence of a table-driven test.
#[test]
fn test_slugify_table() {
let cases = [
("Hello World", "hello-world"),
("Rust & Coffee!", "rust--coffee"),
("", ""),
(" Trim Me ", "--trim-me--"),
("123 Numbers", "123-numbers"),
];
for (input, expected) in cases {
assert_eq!(slugify(input), expected);
}
}
Fixing the 'Which one failed?' problem
Here is where I usually mess up the first time I do this. I just ran the test above, and I intentionally introduced a bug in my slugify function to see what happens. When the test fails, Rust tells me: assertion failed: `(left == right)`. But it doesn't tell me which input caused the failure because it's inside a loop.
If I have 50 cases, I'm stuck playing detective. To fix this, I need to give each test case a name. I'll switch from a tuple to a small struct. This makes the test output much more useful.
struct TestCase {
name: &'static str,
input: &'static str,
expected: &'static str,
}
#[test]
fn test_slugify_refined() {
let cases = [
TestCase { name: "basic", input: "Hello World", expected: "hello-world" },
TestCase { name: "special chars", input: "Rust & Coffee!", expected: "rust--coffee" },
TestCase { name: "empty", input: "", expected: "" },
TestCase { name: "whitespace", input: " Trim Me ", expected: "--trim-me--" },
];
for case in cases {
assert_eq!(
slugify(case.input),
case.expected,
"Failed on case: {}", case.name
);
}
}
Now, by adding that third argument to assert_eq!, I've provided a custom failure message. If the "whitespace" case fails, the console will explicitly tell me Failed on case: whitespace. It's a small change, but it saves you a massive amount of time when your test suite grows.
📋 Practical Task
Implement a Table-Driven Suite for a Password Validator
You have been given a validate_password function that checks if a password is at least 8 characters long and contains at least one digit. Your task is to replace the existing fragmented tests with a single, robust table-driven test function.
fn validate_password(password: &str) -> bool {
password.len() >= 8 && password.chars().any(|c| c.is_ascii_digit())
}
Requirements:
- Create a
TestCasestruct that includes aname,input, andexpected(boolean). - Implement a test function
test_password_validation_tablethat iterates through a list of cases. - Include at least five test cases covering: a valid password, too short, no digits, empty string, and a password with exactly 8 characters and one digit.
- Ensure that if a test fails, the output specifies which case name caused the failure.
There are no comments for now.