Rust track

Lesson 20 · Part 5: Cargo, Testing, Concurrency & Async

Testing

Writing and running tests, coverage and mocking

Part 5: Cargo, Testing, Concurrency & Async5 min readFree lesson

Testing is one of Rust's proudest features. Even though the compiler checks the code strictly, it cannot know the correctness of business logic. Rust supports three types of tests as a standard: unit tests, integration tests, and documentation tests, and all can be run with a single line in cargo test.

Learning Objectives

  • Write unit tests with the #[test] function inside #[cfg(test)] mod tests.
  • Place integration tests in the tests/ directory and validate the library as an external user would.
  • Understand that code blocks in /// documentation comments are executed as doctests.
  • Know frequently used macros such as assert!, assert_eq!, assert_ne!, should_panic, etc.

Key Concepts

1) Unit Tests — Inside the Same File

pub fn add(a: i32, b: i32) -> i32 { a + b }

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn add_positive() {
        assert_eq!(add(2, 3), 5);
    }
}
  • mod tests with #[cfg(test)] is compiled only during cargo test.
  • Can access private items in the same file (parent module) using use super::*; — the strength of unit tests.

2) Integration Tests — tests/ Directory

hello/
├── Cargo.toml
├── src/
│   └── lib.rs
└── tests/
    └── integration_test.rs

Each .rs file in tests/ is compiled like an independent crate. Only the library's public API is available, allowing validation from the perspective of an external user.

3) Doctest — /// Code Blocks

/// # Example
/// ```
/// use mycrate::add;
/// assert_eq!(add(2, 3), 5);
/// ```
pub fn add(a: i32, b: i32) -> i32 { a + b }

The code inside the document comments ``` is automatically executed with doctest. The documentation and tests are created at the same time. (However, it only works in the public API of a library crate.)

4) Frequently Used Macros

Macro Meaning
assert!(expr) Panics if not true
assert_eq!(a, b) Panics if a == b is not true (prints both values)
assert_ne!(a, b) Panics if a != b is not true
#[should_panic] Passes only if the body panics
#[ignore] Usually skipped and only run with cargo test -- --ignored

5) cargo test Command

cargo test                          #All tests
cargo test add_positive             #Only those with 'add_positive' in the name
cargo test --lib                    #Unit tests only
cargo test --test integration_test  #Only specific files in tests/
cargo test --doc                    #Doctest only
cargo test -- --nocapture           #Print even println! inside the tests

Examples

Example 1 — src/lib.rs : Unit Test and Doctest

//Episode 20: Library code + unit tests + documentation tests (doctest)

/// Returns the sum of two integers.
///
/// # Example
/// ```
/// use testing::add;
/// assert_eq!(add(2, 3), 5);
/// ```
pub fn add(a: i32, b: i32) -> i32 {
    a + b
}

/// Checks whether an integer is even.
///
/// # Example
/// ```
/// use testing::is_even;
/// assert!(is_even(4));
/// assert!(!is_even(7));
/// ```
pub fn is_even(n: i32) -> bool {
    n % 2 == 0
}

//It is Rust's convention to place the unit test module in the same file.
//Thanks to `#[cfg(test)]`, it is only compiled when running `cargo test`.
#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn add_positive() {
        assert_eq!(add(2, 3), 5);
    }

    #[test]
    fn add_zero() {
        assert_eq!(add(0, 0), 0);
    }

    #[test]
    fn even_true() {
        assert!(is_even(4));
        assert!(is_even(0));
    }

    #[test]
    fn even_false() {
        assert!(!is_even(7));
    }
}

Example 2 — tests/integration_test.rs : Integration Test

//Files in the `tests/` folder are treated as integration tests.
//They are verified using only the public API of the library, just like an external user would.

use testing::{add, is_even};

#[test]
fn integration_add() {
    assert_eq!(add(10, 20), 30);
    assert_eq!(add(-5, 5), 0);
}

#[test]
fn integration_even() {
    for n in [0, 2, 4, 100] {
        assert!(is_even(n));
    }
    for n in [1, 3, 5] {
        assert!(!is_even(n));
    }
}

Example 3 — src/main.rs : Using a Library

//A small executable file that uses the API of the library (`testing`).
//Run tests with `cargo test`.

use testing::{add, is_even};

fn main() {
    println!("add(2, 3) = {}", add(2, 3));
    println!("is_even(4) = {}", is_even(4));

    println!();
    println!("Run the tests of this crate with the following commands:");
    println!("cargo test                          # All tests");
    println!("cargo test --lib                    # Unit tests only");
    println!("cargo test --test integration_test  # Integration tests only");
    println!("cargo test --doc                    # Doctests only");
}

Common Mistakes

Q. cargo test finds nothing.

A. Check if #[test] is missing in the test function or if there are functions outside #[cfg(test)] mod tests. Integration tests must be inside the tests/ directory.

Q. Doctest does not run.

A. Doctest only works in the public API of a library crate. Crates that are binary-only (having only src/main.rs) ignore doctest. You need src/lib.rs, like in this example.

Q. How can I provide richer messages when assert_eq! fails?

A. It accepts additional arguments.

assert_eq!(add(2, 3), 5, "The sum of two positive numbers");

Q. How do I test a function that is supposed to panic?

A. Attach the #[should_panic] or #[should_panic(expected = "part of the message")] attribute. If the body does not panic, the test is considered a failure.

Summary

  • Unit tests are in the same file with #[cfg(test)] mod tests — can access private items.
  • Integration tests are in the tests/ directory — use only public APIs and verify from an external perspective.
  • Documentation tests are in the /// ``` blocks — combine documentation and verification in one place.
  • All tests run with cargo test in a single line — can run partially with filters/flags.

Try it yourself

Solve the assignment below. The reference solution is shown below.

Example files

The runnable files used in this lesson. Copy them into your own project and experiment.

src/testing/Cargo.tomlTOML
[package]
name = "testing"
version = "0.1.0"
edition = "2021"

[dependencies]
src/testing/src/lib.rsRust
//Episode 20: Library code + unit tests + documentation tests (doctest)

/// Returns the sum of two integers.
///
/// # Example
/// ```
/// use testing::add;
/// assert_eq!(add(2, 3), 5);
/// ```
pub fn add(a: i32, b: i32) -> i32 {
    a + b
}

/// Checks whether an integer is even.
///
/// # Example
/// ```
/// use testing::is_even;
/// assert!(is_even(4));
/// assert!(!is_even(7));
/// ```
pub fn is_even(n: i32) -> bool {
    n % 2 == 0
}

//It is Rust's convention to place the unit test module in the same file.
//Thanks to `#[cfg(test)]`, it is only compiled when running `cargo test`.
#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn add_positive() {
        assert_eq!(add(2, 3), 5);
    }

    #[test]
    fn add_zero() {
        assert_eq!(add(0, 0), 0);
    }

    #[test]
    fn even_true() {
        assert!(is_even(4));
        assert!(is_even(0));
    }

    #[test]
    fn even_false() {
        assert!(!is_even(7));
    }
}
src/testing/src/main.rsRust
//A small executable file that uses the API of the library (`testing`).
//Run tests with `cargo test`.

use testing::{add, is_even};

fn main() {
    println!("add(2, 3) = {}", add(2, 3));
    println!("is_even(4) = {}", is_even(4));

    println!();
    println!("Run the tests of this crate with the following commands:");
    println!("cargo test                          # All tests");
    println!("cargo test --lib                    # Unit tests only");
    println!("cargo test --test integration_test  # Integration tests only");
    println!("cargo test --doc                    # Doctests only");
}
src/testing/tests/integration_test.rsRust
//Files in the `tests/` folder are treated as integration tests.
//They are verified using only the public API of the library, just like an external user would.

use testing::{add, is_even};

#[test]
fn integration_add() {
    assert_eq!(add(10, 20), 30);
    assert_eq!(add(-5, 5), 0);
}

#[test]
fn integration_even() {
    for n in [0, 2, 4, 100] {
        assert!(is_even(n));
    }
    for n in [1, 3, 5] {
        assert!(!is_even(n));
    }
}

Assignment

Problem 1 — abs and Unit Test

  • File name: homework_01/src/main.rs
  • Key concepts: #[test], assert_eq!

Requirements

  • Define the function abs(n: i32) -> i32 (absolute value).
  • Inside #[cfg(test)] mod tests, write the following 4 tests.
    • abs(-7) == 7
    • abs(7) == 7
    • abs(0) == 0
  • Extreme cases like abs(i32::MIN.checked_neg().unwrap_or(0)) can be omitted — just handle simple positive, negative, and 0.
  • Print abs(-9) in main.

Expected output (cargo run)

abs(-9) = 9

Expected result (cargo test)

  • All tests PASS.

Hint

  • if n < 0 { -n } else { n }.
  • Test module: #[cfg(test)] mod tests { use super::*; #[test] fn ... {} }.

Problem 2 — is_prime and Unit Test

  • File name: homework_02/src/main.rs
  • Core concepts: #[test], assert!/assert!(!...)

Requirements

  • Define the function is_prime(n: u32) -> bool.
  • 0 and 1 are false.
  • Start from 2 and divide up to √n.
  • Place the following tests inside #[cfg(test)] mod tests.
    • is_prime(0) == false, is_prime(1) == false
  • All of is_prime(2), is_prime(3), is_prime(7), is_prime(13) are true
    • is_prime(4) == false, is_prime(9) == false
  • In main, print whether 7 is prime.

Expected output (cargo run)

is_prime(7) = true

Expected result (cargo test)

  • All tests PASS.

Hint

  • if n < 2 { return false; }.
  • let mut i = 2; while i * i <= n { if n % i == 0 { return false; } i += 1; } true.

Check your answers

Try solving it yourself first, then compare your answer with the reference solution below. You can check the main result with cargo run and the test result with cargo test.

Show the reference solution

Try the assignment yourself first — comparing your attempt with a working solution is where most of the learning happens.

homework_01/Cargo.tomlTOML
[package]
name = "homework_01"
version = "0.1.0"
edition = "2021"

[dependencies]
homework_01/src/main.rsRust
//Key Points:
//- It is common to put unit tests in the same file as the function definition.
//- You can also put `#[cfg(test)] mod tests` inside main.rs, and `cargo test` automatically recognizes it.

fn abs(n: i32) -> i32 {
    if n < 0 { -n } else { n }
}

fn main() {
    println!("abs(-9) = {}", abs(-9));
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn negative() {
        assert_eq!(abs(-7), 7);
    }

    #[test]
    fn positive() {
        assert_eq!(abs(7), 7);
    }

    #[test]
    fn zero() {
        assert_eq!(abs(0), 0);
    }
}
homework_02/Cargo.tomlTOML
[package]
name = "homework_02"
version = "0.1.0"
edition = "2021"

[dependencies]
homework_02/src/main.rsRust
//Key Points:
//- 0 and 1 are not prime numbers, so branch early.
//- Check only up to √n using the condition i * i <= n — a safe method without overflow concerns.

fn is_prime(n: u32) -> bool {
    if n < 2 {
        return false;
    }
    let mut i: u32 = 2;
    while i * i <= n {
        if n % i == 0 {
            return false;
        }
        i += 1;
    }
    true
}

fn main() {
    println!("is_prime(7) = {}", is_prime(7));
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn below_two() {
        assert!(!is_prime(0));
        assert!(!is_prime(1));
    }

    #[test]
    fn primes() {
        for n in [2, 3, 7, 13] {
            assert!(is_prime(n), "{n} must be a prime number");
        }
    }

    #[test]
    fn composites() {
        assert!(!is_prime(4));
        assert!(!is_prime(9));
    }
}

Learn this lesson with a mentor

Work through the Rust track on your own, or join a guided cohort at our Pitampura campus and online.

Call NowEnroll Now