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

A few years ago, I was working on a custom caching layer for a high-traffic API. I spent an entire Tuesday afternoon chasing a bug that only appeared when the cache was exactly 50% full and a specific key was evicted. I kept trying to reproduce it by manually running the server and hitting endpoints with Curl, which is essentially like trying to find a needle in a haystack by burning the haystack down. It wasn't until I sat down and wrote a targeted test case that I realized I had a classic off-by-one error in my index calculation. That's the moment it clicked for me: if you're manually testing your logic, you're not testing it—you're just hoping it works.

Keeping Logic Close with Unit Tests

In Rust, the convention is to put your unit tests in the same file as the code they are testing. I know that feels messy if you're coming from Java or C#, but it's incredibly powerful because it allows your tests to access private functions and structs. You don't have to make everything pub just to verify it works.

To do this, we wrap our tests in a module—usually named tests—and mark it with the #[cfg(test)] attribute. This tells the Rust compiler, "Hey, only compile this module when I'm running cargo test." This keeps your final binary lean because the test code is completely stripped out of the production build.


pub fn add_to_inventory(count: u32, current: u32) -> u32 {
    count + current
}

#[cfg(test)]
mod tests {
    use super::*; // Import everything from the outer module

    #[test]
    fn test_inventory_addition() {
        assert_eq!(add_to_inventory(5, 10), 15);
    }

    #[test]
    fn test_inventory_zero() {
        assert_eq!(add_to_inventory(0, 10), 10);
    }
}

You'll notice I used assert_eq!. Rust provides a few of these macros; assert! checks if a boolean is true, while assert_eq! tells you exactly what the two values were if they didn't match, which saves you from having to add a bunch of print statements just to see what went wrong.

Treating Your Library as a User

Unit tests are great for the nitty-gritty details, but they have a blind spot: they know too much. Because they can see private internals, they don't tell you if your library is actually easy to use for someone else. That's where integration tests come in.

Integration tests live in a separate directory called tests/ at the root of your project. When you put a file in there, Cargo treats it as a separate crate. This means you can only call the public API of your library. If you find yourself struggling to set up an integration test because you can't access a necessary function, that's a huge red flag that your API design is too restrictive.

I usually use integration tests to verify "happy paths"—the primary workflows a user would actually take. If your library is a database wrapper, an integration test would be: "Can I open a connection, insert a row, and read it back?" without caring how the internal buffer management works.

Turning Documentation into Verified Code

One of my favorite features of Rust is documentation tests. We've all downloaded a library only to find that the examples in the README are outdated and don't actually compile. Rust solves this by allowing you to write tests directly inside your doc comments using triple slashes ///.


/// Adds two numbers together.
///
/// # Examples
///
/// ```
/// let result = my_crate::add(2, 3);
/// assert_eq!(result, 5);
/// ```
pub fn add(a: i32, b: i32) -> i32 {
    a + b
}

When you run cargo test, Rust actually parses those code blocks, compiles them, and runs them. If you change the function signature but forget to update the documentation, your tests will fail. It's a built-in guarantee that your documentation never lies to your users. I've found that writing doc tests first actually helps me design a cleaner API, as it forces me to think about the developer experience from the very first line of code.




📋 Practical Task

Hardening the WordCounter Logic

You are building a WordCounter utility that counts the number of words in a string. However, it needs to handle edge cases: it should ignore punctuation and treat words as case-insensitive.

Your Task:

  1. Implement a function count_words(input: &str) -> usize that returns the number of words.
  2. Create a mod tests block using #[cfg(test)]. Write unit tests to verify that:
    • An empty string returns 0.
    • A string with only spaces returns 0.
    • A string with mixed punctuation (e.g., "Hello, world!") correctly identifies 2 words.
  3. Add a documentation test (using ///) that demonstrates a basic usage of count_words and asserts the result is correct.
  4. Create a mock integration test scenario (conceptually) by ensuring your function is pub and can be called from an external context.

Hint: Consider using input.split_whitespace() and filtering out non-alphanumeric characters if you want to be thorough with punctuation.

Rating
0 0

There are no comments for now.

to be the first to leave a comment.