Skip to Content
Course content

207: Table-Driven Tests in Rust

Click on the "Edit" button in the top corner of the screen to edit your slide content.

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 TestCase struct that includes a name, input, and expected (boolean).
  • Implement a test function test_password_validation_table that 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.
Rating
0 0

There are no comments for now.

to be the first to leave a comment.