Skip to content

A first test, step by step

Level: 101 → 201 · a path through one test

One line: The first test in Rust for Machine Learning checks that four lines of CSV load into a Vec<Vec<f32>>. It is written before the function it tests, so the test build goes through three states: it does not compile, it compiles and fails, and it passes. Four pages, one per step, and every new idea on the way is explained where it first appears.

fn parse_rows(csv: String) -> Vec<Vec<f32>> {
    csv.lines()
        .map(|line| line.split(',').map(|field| field.trim().parse().unwrap()).collect())
        .collect()
}

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

    #[test]
    fn loads_the_or_table() {
        let rows = parse_rows(String::from("1,1,1\n-1,-1,-1\n"));
        assert_eq!(rows[1], [-1.0, -1.0, -1.0]);
    }
}

Run it with rustc --edition 2024 --test first_test.rs -o /tmp/t && /tmp/t, which prints test tests::loads_the_or_table ... ok.

This follows chapter 1 of the book from "A Dataset for AND and OR Functions" to "Making the Test Pass". The chapter's opening, the and and or functions, is checked on its own page. The code on these pages is written for this library, so names differ from the book's (parse_rows here, a longer name there), but each step does the same thing the chapter does.

The data, before the Rust

The test loads the truth table for OR, and the chapter introduces machine-learning vocabulary for it:

Term Also called In the OR table
dataset the whole table, four rows
feature attribute, input variable the columns A and B
label outcome, output variable the column A or B
example instance one row, such as True, False, True
class a value the label can take: True or False

The program cannot read True and False as numbers, so the chapter writes True as 1 and False as -1, one example per line, with the label last:

1,1,1
1,-1,1
-1,1,1
-1,-1,-1

1 and -1 rather than 1 and 0 is a choice. It suits the perceptron the chapter builds next, whose output is either −1 or 1. Loaded, each line becomes a Vec<f32> of three numbers, and the table becomes a Vec<Vec<f32>>.

The steps

Step Page The test build New on that page
1 The test comes first does not compile: E0425, plus an unused-import warning #[cfg(test)], mod tests, use super::*, #[test]
2 What the test body says — the \ line continuation, Vec<Vec<f32>>, indexing, a Vec compared with an array, the type of 1.0
3 A stub that panics compiles, and the test fails: not yet implemented todo!(), the _ prefix on a parameter, reading the harness report
4 Making it pass passes lines, split, trim, parse, unwrap, two collects, and the split('\n') that fails

What the chapter says, run

Each row links to the section that runs it.

The chapter says Verdict Run on
Unit tests go in a module in the same file holds step 1
#[cfg(test)] compiles the module only when tests are run narrower: it is compiled in any test build, and cargo test --no-run compiles it without running anything step 1
use super::* imports the code from the file narrower: it brings the names of the parent module into scope, private ones included, and super is the parent module, which is the file only at the top level step 1
Attributes influence compilation without changing the logic does not hold: #[cfg] removes code, and #[derive] writes it step 1
The unused-import warning appears because the file is empty narrower: the file has main; the warning means nothing the glob brought in is used yet step 1
Rust has four primary scalar types and two primitive compound types holds: bool, integers, floats and char; tuples and arrays Values
String stores text as a vector of bytes holds: its one field is a Vec<u8>, which is always valid UTF-8 String vs &str
assert_eq! checks that its two arguments have the same value holds, and they need not be the same type: it compares with ==, so a Vec can equal an array step 2
Rust defaults to f64 narrower: a float literal with nothing to decide its type falls back to f64, and the test's 1.0 is an f32 step 2
todo!() fails with not yet implemented holds step 3
Iterator is a type does not hold: it is a trait, and lines() returns Lines, a struct that implements it step 4
unwrap() tells the compiler the parse will work does not hold: the compiler checks nothing, and a bad string panics at run time step 4
The explicit parse snippet, Result<f32, ParseFloatError> does not compile as printed: it needs use std::num::ParseFloatError; step 4

The steps, in order, with the commands

The chapter runs everything through Cargo. Each command below was run with Cargo 1.98.0 on the files from the step pages; paths and timings are trimmed from the output.

1. Make a folder and write the test first. Put the test from step 1 in main.rs, with an empty fn main() {} above it.

2. Turn the folder into a Cargo package.

cargo init

Because main.rs already exists, cargo init does not create src/. It writes a Cargo.toml whose [[bin]] section points at the file, path = "main.rs". Outside an existing Git repository it also runs git init and writes a .gitignore; inside one it does neither. From one .rs file to a Cargo project covers cargo init in full, including the file name it silently ignores, and Running a scratch program compares it with cargo new and with bare rustc.

3. Run the tests: the build fails.

cargo test
Abridged — cargo test, first run (exit status 101)
warning: unused import: `super::*`
 --> main.rs:5:9
error[E0425]: cannot find function `parse_rows` in this scope
  --> main.rs:15:35
warning: `play` (bin "play" test) generated 1 warning
error: could not compile `play` (bin "play" test) due to 1 previous error; 1 warning emitted

(bin "play" test) is Cargo saying which build failed: the test build of the binary target. Step 1 reads both messages.

4. Add a stub with the right signature and todo!() as its body, above the test.

5. Run the tests again: it builds, and the test fails.

Abridged — cargo test, second run (exit status 101)
     Running unittests main.rs (target/debug/deps/play-74c017eb9950b1a4)

running 1 test
test tests::loads_the_or_table ... FAILED

---- tests::loads_the_or_table stdout ----
thread 'tests::loads_the_or_table' (29429874) panicked at main.rs:4:5:
not yet implemented

test result: FAILED. 0 passed; 1 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s

error: test failed, to rerun pass `--bin play`

Running unittests main.rs names the file whose unit tests are running, and the path in brackets is the test binary Cargo built. The last line is Cargo's advice for running only this target again. Step 3 reads the report line by line.

6. Replace the stub with the loader from step 4.

7. Run the tests a third time: it passes.

Abridged — cargo test, third run (exit status 0)
running 1 test
test tests::loads_the_or_table ... ok

test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s

Without Cargo, the same three states come from rustc --edition 2024 --test main.rs -o /tmp/t && /tmp/t: --test builds the test harness in place of your main.

Every keyword and command, and where it is explained

Word or command What it is here Explained on
Commands
cargo init makes the current folder a Cargo package step list above · From one .rs file to a Cargo project
cargo test builds the test harness and runs every #[test] Where a test goes · cargo-nextest
cargo test --no-run builds the test binary and runs nothing step 1
cargo build the normal build, where #[cfg(test)] code does not exist step 3
rustc --test the same test harness, without Cargo Running a scratch program
Attributes and modules
#[cfg(test)] keep the item only in a test build step 1 · What an attribute is
#[test] mark a function as a test step 1 · Where a test goes
mod tests a child module holding the tests Modules and visibility
use super::* import every name from the parent module step 1 · The use declaration
Functions and closures
fn, -> a function and its return type Functions
_csv a parameter the body does not use yet step 3 · What a warning is asking
|line| … a closure, the function map calls on each item What a closure is
Types
let x: Vec<Vec<f32>> a variable with a type annotation Variables · What a type annotation does
bool, integers, f32/f64, char the four scalar types Values · What a float stores
tuples, arrays, slices the compound types the chapter names Tuples · Arrays and slices
String, &str, String::from owned text, borrowed text, and one way to make the first String vs &str · Making a String
Vec<T>, <T> a growable list, and the generic parameter in its name Vec · What a generic is
Vec<Vec<f32>> a table of rows step 2 · Grids and nested Vecs
HashMap the key-value map the chapter mentions HashMap
Result<T, E>, ParseFloatError the value or the reason there is none Ok and Err · Option vs Result
Macros
name!(…) a macro call, not a function call Macros · The braces take a name
assert_eq! panic unless the two sides are == step 2 · What a test asserts
todo!() a placeholder that panics step 3 · The never type !
vec![…] build a Vec from a list Vec
Strings and iterators
.lines() split text into lines, dropping \n step 4 · str::lines
.split(',') the pieces between commas str::split
.trim() remove whitespace at both ends str::trim
.parse() text to a number, as a Result Parsing a string · str::parse
.map(…) an adapter: transform each item Adapters by job · Iterators are lazy
.collect() a consumer: build a collection Collect into a Vec
Iterator the trait Lines and Split implement Iterators · step 4
Errors and failures
.unwrap() the value, or a panic What unwrap does · unwrap is a TODO
panic, exit status 101 how a Rust program or a test fails What a panic costs
E0425 a name that is not in scope step 1 · Error codes

See also

Po polsku

Pierwszy test w książce Rust for Machine Learning sprawdza, czy cztery wiersze CSV z tablicą prawdy dla OR wczytują się do Vec<Vec<f32>>. Test powstaje przed funkcją, którą sprawdza (to test-driven development, TDD), więc kompilacja testowa przechodzi przez trzy stany: nie kompiluje się (E0425), kompiluje się i test pada (not yet implemented z todo!()), wreszcie przechodzi. Każdy krok ma własną stronę, a każde nowe pojęcie jest wyjaśnione tam, gdzie pojawia się pierwszy raz.

Słownictwo uczenia maszynowego z tego rozdziału: zbiór danych (dataset) to cała tabela; cechy (features, też attributes, input variables) to kolumny A i B; etykieta (label, outcome) to kolumna z wynikiem; przykład (example, instance) to jeden wiersz; klasa (class) to wartość, jaką może przyjąć etykieta — tu True albo False. W pliku True zapisuje się jako 1, a False jako -1, z etykietą w ostatniej kolumnie.

Kilka zdań z rozdziału nie wytrzymuje uruchomienia: atrybuty zmieniają program (#[cfg] usuwa kod, #[derive] go dopisuje), Iterator to cecha (trait), a nie typ, a unwrap() niczego nie obiecuje kompilatorowi — to sprawdzenie w czasie działania, które panikuje. Inne są węższe, niż brzmią: #[cfg(test)] decyduje przy kompilacji, a nie przy uruchamianiu testów; use super::* importuje nazwy z modułu nadrzędnego, a nie „kod z pliku”; literał 1.0 jest typu f64 tylko wtedy, gdy nic innego nie wskazuje typu — w tym teście jest f32.

Szukaj po polsku: testy jednostkowe w Ruscie · test-driven development · rust cfg test mod tests · rust use super::* · rust todo! not yet implemented · rust vec compare array assert_eq