Prove Behavior with Rust Tests

RustBeginner
Practice Now

Introduction

Tests turn an expectation into executable evidence. A focused test calls one behavior, compares its actual result with the expected result, and makes future changes safer by detecting regressions automatically.

You will add tests to a prepared shipping-quote library. The production functions are intentionally small so you can focus on where Rust tests live, how assertions communicate expectations, which boundary cases matter, and how Cargo can run one test target at a time.

Add a Focused Unit Test

In this step, you will place a unit test beside the library code and run only that named test.

The project is /home/labex/project/shipping-quote. Move into it:

cd /home/labex/project/shipping-quote

Before editing, inspect the short library source. The command sed -n '1,220p' is used only as a read-only viewer here: -n suppresses automatic printing, and 1,220p explicitly prints lines 1 through 220. That range is long enough to show this complete file without opening an editor:

sed -n '1,220p' src/lib.rs

Syntax written as #[...] is an attribute: metadata attached to the Rust item immediately below it. The #[cfg(test)] attribute tells Rust to compile the following test module only during test builds. A module is a named container for related items; the next Guided Lab teaches modules as a general organization tool.

Inside mod tests, the path word super means the parent module, and * means all accessible names from that parent. Therefore use super::*; makes the library functions available by their short names inside the test module. Finally, a function marked with #[test] becomes a test that Cargo can discover and run.

Open the source with Nano:

nano src/lib.rs

Replace // FIRST_UNIT_TEST with this small test:

    #[test]
    fn light_parcel_costs_five() {
        assert_eq!(shipping_cost(3), 5);
    }

assert_eq! compares its two expressions. If they are equal, the test continues; if not, the test fails and prints both values. Here, the test records that a three-kilogram parcel costs five units to ship.

Save with Ctrl+O, press Enter, and exit with Ctrl+X. Pass the test name after cargo test to run only tests whose names contain that text:

cargo test light_parcel_costs_five

Cargo compiles the test build and reports the named unit test:

test tests::light_parcel_costs_five ... ok

test result: ok. 1 passed; 0 failed; ...

The tests:: prefix shows that the test lives inside the library's local test module, and ok proves its assertion passed.

Cover Boundary Cases

In this step, you will add tests at the boundaries where shipping behavior changes.

A representative value such as 3 proves one point inside the light-parcel range, but bugs often hide at edges. In the prepared match, 1..=5 is a range pattern that matches any value from one through five, including both endpoints. It reuses the inclusive-range notation from loops in a new pattern context. The function has special behavior at zero and keeps the five-unit rate through exactly five kilograms. Testing those values records both boundaries.

Open the library again:

nano src/lib.rs

Replace // EDGE_UNIT_TESTS with two short tests:

    #[test]
    fn empty_parcel_is_free() {
        assert_eq!(shipping_cost(0), 0);
    }

    #[test]
    fn five_kilograms_is_still_light() {
        assert_eq!(shipping_cost(5), 5);
    }

Each test describes one rule in its name and contains one focused assertion. This makes a failure easy to locate. Save and exit Nano.

The filter tests:: matches the full names of tests in the local unit-test module. Use it to run all three unit tests while excluding the separate integration-test target for now:

cargo test tests::

The stable evidence is three passing unit tests:

test tests::empty_parcel_is_free ... ok
test tests::five_kilograms_is_still_light ... ok
test tests::light_parcel_costs_five ... ok

test result: ok. 3 passed; 0 failed; ...

Together, the examples cover an ordinary value and the two important edges of the light-parcel rule.

Complete an Integration Test

In this step, you will test the library through its public interface from a separate file.

Unit tests inside src/lib.rs are close to the implementation and can use the parent module's internal items. Integration tests live in the top-level tests/ directory, compile as separate test programs, and use only the library's public interface like an external caller. The package name shipping-quote becomes the Rust crate name shipping_quote, with an underscore, in source code. The next Lab consolidates package, crate, module, and public-visibility terminology.

Inspect the prepared integration-test scaffold:

sed -n '1,160p' tests/order_workflow.rs

It imports only the public order_total function, calls it for a 40-unit order and a three-kilogram parcel, then leaves one assertion for you to complete. Open the file:

nano tests/order_workflow.rs

Replace the placeholder assertion line ending in // INTEGRATION_ASSERTION with:

    assert_eq!(total, 45);

The expected total combines the 40-unit subtotal with the five-unit light-parcel shipping cost. Save and exit Nano.

The --test order_workflow option selects the integration-test target whose file is tests/order_workflow.rs:

cargo test --test order_workflow

The target reports its public-workflow test:

test order_total_includes_shipping ... ok

test result: ok. 1 passed; 0 failed; ...

Finally, run the entire package test suite without a filter:

cargo test

Cargo runs the three unit tests and the integration test. Separate result summaries are normal because they are different test binaries; every named test should show ok and every summary should show zero failures.

Summary

You added local unit tests, covered rule boundaries, completed an integration test through the library's public API, and used Cargo filters to run one test, one module, one integration target, or the complete suite. These habits provide fast evidence while developing and broader regression protection before handoff.