An Introduction to the Rust language – part 1.
(Note: All contents in this post comes from the Rust Programming Language)
4. Understanding Ownership
What’s Ownership
Ownership Rules:
- Each value in Rust has a variable that’s called its owner.
- There can only be one owner at a time.
- Where the owner goes out of scope, the value will be droped.
Memory and Allocation:
Ways Variables and Data Interact: Move
1 | // This String's owner from s1 move to s2 |
Ways Variables and Data Interact: Clone
1 | // do a deeply copy of the heap data of the String (expensive) |
Stack-Only Data: Copy
1 | let x = 1; |
References and Borrowing
Add ampersand before variable let the variable become a reference which refers
to the value but does not own it.
Just as variables are immutable by default, so as references.
Mutable References
1 | fn main() { |
A reference’s scope starts from where it is introduced and continues through the
last time that reference is used.
- At any given time, you can have either one mutable reference or any number of
immutable references. - References must always be valid.
Dangling References
1 | fn main() { |
The Slice Type
A string slice is a reference to part of a String.
1 | let s = String::from("hello world"); |
1 | fn main() { |
String literal is slice that has &str
type, so it is immutable.
Change the parameter’s type of function first_word for more general.
1 | fn main() { |
Other slice:1
2let a = [1, 2, 3, 4, 5];
let slice = &a[1..3];
This slice has the type &[i32]
5. Using Structs to Structure Related Data
Defining and Instantiating Structs
1 | struct User { |
Use the field init shorthand syntax when variables and fields have the same name.1
2
3
4
5
6
7
8fn build_user(email: String, username: String) -> User {
User {
email,
username,
active: true,
sign_in_count: 1,
}
}
Create instances from other instances with struct update syntax.1
2
3
4
5let user2 = User {
email: String::from("another@example.com"),
username: String::from("anotherusername567"),
..user1
};
Tuple Struct
:1
2
3
4
5struct Color(i32, i32, i32);
struct Point(i32, i32, i32);
let black = Color(0, 0, 0);
let origin = Point(0, 0, 0);
Structs that don’t have any fields are called unit-like structs.
Method
Methods are similar to functions, but they are defined within the context of astruct
(or an enum
or a trait obejct), and their first parameter is alwaysself
, which represents the instance of the struct the method is being called on.
1 |
|
Associated Functions
Associated functions are implemented within impl blocks, which don’t take
self as a parameter. And they are associated with the struct.
1 | impl Rectangle { |
Access it with ::
syntax:1
let sq = Rectangle::square(3);
::
syntax is used for both associated functions and namespaces created by modules.
6. Enums and Pattern Matching
Defining an Enum
1 | enum IpAddrKind { |
Putting data directly into each enum variant.1
2
3
4
5
6
7
8enum IpAddr {
V4(String),
V6(String),
}
let home = IpAddr::V4(String::from("127.0.0.1"));
let loopback = IpAddr::V6(String::from("::1"));
Each variant can have different types and amounts of associated data.1
2
3
4
5
6
7
8enum IpAddr {
V4(u8, u8, u8, u8),
V6(String),
}
let home = IpAddr::V4(127, 0, 0, 1);
let loopback = IpAddr::V6(String::from("::1"));
The Option
Enum:
It is defined by the standard library.1
2
3
4enum Option<T> {
Some(T),
None,
}
1 | let some_number = Some(5); |
The match Control Flow Operator
1 | enum Coin { |
1 | fn value_in_cents(coin: Coin) -> u8 { |
Pattern that bind to values:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25// so we can inspect the state in a minute
enum UsState {
Alabama,
Alaska,
// --snip--
}
enum Coin {
Penny,
Nickel,
Dime,
Quarter(UsState),
}
fn value_in_cents(coin: Coin) -> u8 {
match coin {
Coin::Penny => 1,
Coin::Nickel => 5,
Coin::Dime => 10,
Coin::Quarter(state) => {
println!("State quarter from {:?}!", state);
25
},
}
}
Call it with1
value_in_cents(Coin::Quarter(UsState::Alaska))
Matching with Option<T>
:1
2
3
4
5
6
7
8
9
10fn plus_one(x: Option<i32>) -> Option<i32> {
match x {
None => None,
Some(i) => Some(i + 1),
}
}
let five = Some(5);
let six = plus_one(five);
let none = plus_one(None);
The _
Placeholder1
2
3
4
5
6
7
8let some_u8_value = 0u8;
match some_u8_value {
1 => println!("one"),
3 => println!("three"),
5 => println!("five"),
7 => println!("seven"),
_ => (), // () is the unit value
}
Concise Control Flow with if let
1 | let some_u8_value = Some(0u8); |
1 | if let Some(3) = some_u8_value { |
1 | let mut count = 0; |
same as above1
2
3
4
5
6let mut count = 0;
if let Coin::Quarter(state) = coin {
println!("State quarter from {:?}!", state);
} else {
count += 1;
}
7. Managing Growing Projects with Packages, Crates, and Modules
Module System:
- Packages: A Cargo feature that lets you build, test, and share crates
- Crates: A tree of modules that produces a library or executable
- Modules and use: Let you control the organization, scope, and privacy of
paths - Paths: A way of naming an item, such as a struct, function, or module
Packages and Crates
A crate is a binary or library.
A package is one or more crates that provide a set of functionality. A package
contains a Cargo.toml file that describes how to build those crates.
A package can contain multiple binary crates and optionally one library crate. It
must contain at least one crate (either library or binary).
Use the following command to create a package:1
cargo new your_project_name
src/main.rs is the crate root of a binary crate with the same name as the package.
whereas, src/lib.rs the crate root of a library crate. Cargo passes the crate
root files to rustc
to build the library or binary. A package can have multiple
binary crates by replacing files in the src/bin directory: each file will be a
separate binary crate.
A crate will group related functionality together in a scope so the functionality
is easy to share between multiple projects.
Defining Modules to Control Scope and Privacy
The use
keyword brings a path into scope; and the pub
keyword makes items public.
Modules let us organize code within a crate into groups for readability and
easy reuse. Modules also control the privacy of items, which is whether an item
can be used by outside code (public) or not (private).
To create a new library named restaurant by typing the following command:1
cargo new --lib restaurant
In src/lib.rs:1
2
3
4
5
6
7
8
9
10
11
12mod front_of_house {
mod hosting {
fn add_to_waitlist() {}
fn seat_at_table() {}
}
mod serving {
fn take_order() {}
fn serve_order() {}
fn take_payment() {}
}
}
Modules can contain other modules within it. It can also hold definitions for
other items, such as structs, enums, constants, traits, or functions.
src/main.rs and src/lib.rs are called crate roots. Because the contents of
either of these two files form a module named crate
at the root of the crate’s
module structure, known as the module tree.1
2
3
4
5
6
7
8
9crate
└── front_of_house
├── hosting
│ ├── add_to_waitlist
│ └── seat_at_table
└── serving
├── take_order
├── serve_order
└── take_payment
Paths for Referring to an Item in the Module Tree
A path can take two forms:
- An absolute path starts from a crate root by using a crate name or a literal
crate. - A relative path starts from the current module and uses
self
,super
, or
an identifier in the current module.
Items (functions, methods, structs, enums, modules, and constants) in a parent
module can’t use the private items inside child modules, however, items in child
modules can use the items in their ancestor modules. All items are private by default.
Exposing Paths with the pub
keyword:1
2
3
4
5
6
7
8
9
10
11
12
13mod front_of_house {
pub mod hosting {
pub fn add_to_waitlist() {}
}
}
pub fn eat_at_restaurant() {
// absolute path
crate::front_of_house::hosting::add_to_waitlist();
// relative path
front_of_house::hosting::add_to_waitlist();
}
Starting relative paths with super
:1
2
3
4
5
6
7
8
9
10fn serve_order() {}
mod back_of_house {
fn fix_incorrect_order() {
cook_order();
super::serve_order();
}
fn cook_order() {}
}
Making structs and enums public1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23mod back_of_house {
pub struct Breakfast {
pub toast: String,
seasonal_fruit: String,
}
impl Breakfast {
pub fn summer(toast: &str) -> Breakfast {
Breakfast {
toast: String::from(toast),
seasonal_fruit: String::from("peaches"),
}
}
}
}
pub fn eat_at_restaurant() {
let mut meal = back_of_house::Breakfast::summer("Rye");
meal.toast = String::from("Wheat");
println!("I'd like {} toast please", meal.toast);
meal.seasonal_fruit = String::from("blueberries"); // it doesn't work
}
In contrast, if we make an enum public, all of its variants are then public.1
2
3
4
5
6
7
8
9
10
11mod back_of_house {
pub enum Apptizer {
Soup,
Salad,
}
}
pub fn eat_at_restaurant() {
let order1 = back_of_house::Apptizer::Soup;
let order2 = back_of_house::Apptizer::Salad;
}
Bringing paths into scope with the use
keyword:1
2
3
4
5
6
7
8
9
10
11
12mod front_of_house {
pub mod hosting {
pub fn add_to_waitlist() {}
}
}
use crate::front_of_house::hosting;
pub fn eat_at_restaurant() {
hosting::add_to_waitlist();
hosting::add_to_waitlist();
}
Providing new names with the as
keyword:1
2
3
4
5use std::fmt::Result;
use std::io::Result as IoResult;
fn function1() -> Result {}
fn function2() -> IoResult<()> {}
Re-exporting names with pub use
:
When we bring a name into scope with the use
keyword, the name available in the
new scope is private. To make it available for other code that calls our code,
we can combine pub
and use
.1
2
3
4
5
6
7
8
9
10
11
12mod front_of_house {
pub mod hosting {
pub fn add_to_waitlist() {}
}
}
pub use crate::front_of_house::hosting;
pub fn eat_at_restaurant() {
hosting::add_to_waitlist();
hosting::add_to_waitlist();
}
Using external packages:
Listing external packages’ name in your package’s Cargo.toml file and usinguse
to bring items into scope.
The standard library (std
) is also a crate that’s external to our package, but
it is shipped with the Rust language. So we only need to bring items within it
into scope with use
.1
use std::collections::HashMap;
Using nested paths to clean up large use
lists:1
use std::{cmp::Ordering, io};
1 | use std::io::{self, Write}; |
equal to1
2use std::io;
use std::io::Write;
The glob operator:
Bring all public items defined in a path into scope.1
use std::collections::*;
Separating Modules into Different Files
In src/lib.rs:1
2
3
4
5
6
7
8
9
10// declaring the "front_of_house" module whose body will be in
// "src/front_of_house.rs" file
mod front_of_house;
pub use crate::front_of_house::hosting;
pub fn eat_at_restaurant() {
hosting::add_to_waitlist() {};
hosting::add_to_waitlist() {};
}
In src/front_of_house.rs:1
2
3pub mod hosting {
pub fn add_to_waitlist() {}
}
You can continue this process:
In src/front_of_house.rs:1
pub mod hosting;
In src/front_of_house/hosting.rs:1
pub fn add_to_waitlist() {}
Rust lets you split a package into multiple crates and a crate into modules.
8. Common Collections
Unlike the build-in array and tuple types, the data these collections point to
is stored on the heap, which means the amount of data does not need to be known
at compile time and can grow or shrink as the program runs.
- A vector allows you to store a variable number of values next to each other.
- A string is a collection of characters.
- A hash map allows you to associate a value with a particular key.
Storing Lists of Values with Vectors
Vec<T>
:
1 | let v: Vec<i32> = Vec::new() // must add type annotation here |
using vec!
macro1
2// the type of v is Vec<i32>
let v = vec![1, 2, 3];
1 | let mut v = Vec::new(); |
A vector is freed when it goes out of scope. When it gets dropped, all of its
contents are also dropped.1
2
3{
let v = vec![1, 2, 3];
} // v goes out of scope and is freed here
There are two ways to reference a value stored in a vector: indexing syntax andget
method (return an Option<&T>).1
2
3
4
5
6
7
8
9
10
11let v = vec![1, 2, 3, 4, 5];
// indexing syntax
let third: &i32 = &v[2];
println!("The third element is {}", third);
// "get" method
match v.get(2) {
Some(third) => println!("The third element is {}", third),
None => println!("There is no third element."),
}
1 | let v = vec![1, 2, 3, 4]; |
The following code can’t work:1
2
3
4let mut v = vec![1, 2, 3, 4, 5];
let first = &v[0]; // immutable borrow
v.push(6); // mutable borrow
println!("The first element is: {}", first);
Iterating over the values in a vector:1
2
3
4let v = vec![100, 32, 57];
for i in &v {
println!("{}", i);
}
1 | let mut v = vec![1, 2, 3]; |
Using an enum to store multiple types:1
2
3
4
5
6
7
8
9
10
11enum SpreadsheetCell {
Int(i32),
Float(f64),
Text(String),
}
let row = vec![
SpreadsheetCell::Int(3),
SpreadsheetCell::Text(String::from("blue")),
SpreadsheetCell::Float(10.12),
];
9. Error Handling
Rust groups errors into two major categories: recoverable and unrecoverable
errors. Rust has the type Result<T, E>
for recoverable errors and thepanic!
macro that stops execution when the program encounters an unrecoverable error.
Unrecoverable Errors with panic!
Using a panic!
Backtrace
1 | RUST_BACKTRACE=1 cargo run |
Recoverable Errors with Result
1 | use std::fs::File; |
Matching on Different Errors
1 | use std::fs::File; |
same as above (prefered):1
2
3
4
5
6
7
8
9
10
11
12
13
14use std::fs::File;
use std::io::ErrorKind;
fn main() {
let f = File::open("hello.txt").unwrap_or_else(|error| {
if error.kind() == ErrorKind::NotFound {
File::create("hello.txt").unwrap_or_else(|error| {
panic!("Problem creating the file: {:?}", error);
})
} else {
panic!("Problem opening the file: {:?}", error);
}
});
}
Shortcuts for Panic on Error: unwrap
and expect
If the Result value is the Ok
variant, unwrap
will return the value inside
the Ok. If the Result is the Err
variant, unwrap will call the panic!
macro.1
2
3
4
5use std::fs::File;
fn main() {
let f = File::open("hello.txt").unwrap();
}
expect
, which is similar to unwrap, let us also choose the panic! error message.1
2
3
4
5use std::fs::File;
fn main() {
let f = File::open("hello.txt").expect("Failed to open hello.txt");
}
Propagating Errors
1 | use std::fs::File; |
A Shortcut for Propagating Errors: the ?
Operator
Placing the ?
after a Result
. If the value of the Result is an Ok, the
value inside the Ok will get returned from this expression. If the value is an
Err, the Err will be returned from the function, in other words, gets propagated.
same as above:1
2
3
4
5
6
7
8
9
10
11
12use std::fs::File;
use std::io;
use std::io::Read;
fn main() {}
fn read_username_from_file() -> Result<String, io::Error> {
let mut f = File::open("hello.txt")?;
let mut s = String::new();
f.read_to_string(&mut s)?;
Ok(s)
}
shorter:1
2
3
4
5
6
7
8
9
10
11use std::fs::File;
use std::io;
use std::io::Read;
fn main() {}
fn read_username_from_file() -> Result<String, io::Error> {
let mut s = String::new();
File::open("hello.txt")?.read_to_string(&mut s)?;
Ok(s)
}
another way:1
2
3
4
5
6
7
8use std::fs;
use std::io;
fn main() {}
fn read_username_from_file() -> Result<String, io::Error> {
fs::read_to_string("hello.txt")
}
The ?
Operator Can Be Used in Functions That Return Result
One valid return type for main is ()
, and conveniently, another valid return
type is Result<T, E>
.1
2
3
4
5
6
7use std::error::Error;
use std::fs::File;
fn main() -> Result<(), Box<dyn Error>> {
let f = File::open("hello.txt")?;
Ok(())
}
To panic!
or Not to panic!
Create Custom Types for Validation
1 | pub struct Guess { |
10. Generic Types, Traits, and Lifetimes
Generic Data Types
In function definitions:1
2
3
4
5
6
7
8
9
10fn largest<T: PartialOrd>(list: &[T]) -> T {
let mut largest = list[0];
for &item in list.iter() {
if item > largest {
largest = item;
}
}
largest
}
In struct definitions:1
2
3
4struct Point<T, U> {
x: T,
y: U,
}
In enum definitions:1
2
3
4enum Result<T, E> {
Ok(T),
Err(E),
}
In method definitions:1
2
3
4
5
6
7
8
9
10struct Point<T> {
x: T,
y: T,
}
impl<T> Point<T> {
fn x(&self) -> &T {
&self.x
}
}
or we could implement methods only on Point1
2
3
4
5impl Point<f32> {
fn distance_from_origin(&self) -> f32 {
(self.x.powi(2) + self.y.powi(2)).sqrt()
}
}
Traits: Defining Shared Behavior
Defining a trait:1
2
3pub trait Summary {
fn summarize(&self) -> String;
}
Implementing a trait on a type:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26pub struct NewsArticle {
pub headline: String,
pub location: String,
pub author: String,
pub content: String,
}
impl Summary for NewsArticle {
fn summarize(&self) -> String {
format!("{}, by {} ({})", self.headline, self.author, self.location)
}
}
//
pub struct Tweet {
pub username: String,
pub content: String,
pub reply: bool,
pub retweet: bool,
}
impl Summary for Tweet {
fn summarize(&self) -> String {
format!("{}: {}", self.username, self.content)
}
}
Warning:
One restriction to note with trait implementations is that we can implement a
trait on a type only if either the trait or the type is local to our crate. In
other words, we can’t implement external traits on external types.
Default implementations:1
2
3
4
5pub trait Summary {
fn summarize(&self) -> String {
String::from("(Read more...)")
}
}
To use a default implementation1
impl Summary for NewsArticle {}
Default implementations can call other methods in the same trait, even if those
other methods don’t have a default implementation.1
2
3
4
5
6pub trait Summary {
fn summarize_author(&self) -> String;
fn summarize(&self) -> String {
format!("(Read more from {}...)", self.summarize_author())
}
}
1 | impl Summary for Tweet { |
Traits as parameters:
Define a notify funciton that calls the summarize method on its item
parameter, which is of some type that implements the Summary trait. Useimpl Trait
syntax.1
2
3pub fn notify(item: impl Summary) {
println!("Breaking news! {}", item.summarize());
}
(item parameter accepts any type that implements the specified trait.)
Trait bound syntax:1
2
3pub fn notify<T: Summary>(item: T) {
println!("Breaking news! {}", item.summarize());
}
Specifying multiple trait bounds with the + syntax:1
pub fn notify(item: impl Summary + Display) {}
1 | pub fn notify<T: Summary + Display>(item: T) {} |
Clearer trait bounds with where
clauses:1
2
3
4fn some_function<T, U>(t: T, u: U) -> i32
where T: Display + Clone,
U: Clone + Debug
{}
instead of1
fn some_function<T: Display + Clone, U: Clone + Debug>(t: T, u: U) -> i32 {}
Returning types that implement traits:1
2
3
4
5
6
7
8fn returns_summarizable() -> impl Summary {
Tweet {
username: String::from("horse_ebooks"),
content: String::from("of course, as you probably already know, people"),
reply: false,
retweet: false,
}
}
Using trait bounds to conditionally implement methods:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25use std::fmt::Display;
struct Pair<T> {
x: T,
y: T,
}
impl<T> Pair<T> {
fn new(x: T, y: T) -> Self {
Self {
x,
y,
}
}
}
impl<T: Display + PartialOrd> Pair<T> {
fn cmp_display(&self) {
if self.x >= self.y {
println!("The largest member is x = {}", self.x);
} else {
println!("The largest member is y = {}", self.y);
}
}
}
blanket implementations:
(conditionally implement a trait for any type that implements another trait.)1
2
3impl<T: Display> ToString for T {
// --snip--
}
Validating References with Lifetimes
Preventing Dangling References with Lifetimes
The following code is wrong1
2
3
4
5
6
7
8
9
10{
let r;
{
let x = 5;
r = &x;
}
println!("r: {}", r);
}
The Borrow Checker
The Rust compiler has a borrow checker that compares scopes to determine
whether all borrows are valid.
Generic Lifetimes in Functions
Lifetime Annotation Syntax
1 | &'a mut i32 // a mutable reference with an explicit lifetime |
Lifetime Annotations in Function Signatures
1 | fn longest<'a>(x: &'a str, y: &'a str) -> &'a str { |
When we pass concrete references to longest, the concrete lifetime that is
substituted for ‘a is the part of the scope of x that overlaps with the scope
of y.
Thinking in Terms of Lifetimes
When returning a reference from a function, the lifetime parameter for the return
type needs to match the lifetime parameter for one of the parameters.
Lifetime Annotations in Struct Definitions
It’s possible for structs to hold references, but in that case we would need to
add a lifetime annotation on every reference in the struct’s definition.1
2
3
4
5
6
7
8
9
10
11struct ImportantExcerpt<'a> {
part: &'a str,
}
fn main() {
let novel = String::from("Call me Rick. Some years ago...");
let first_sentence = novel.split('.')
.next()
.expect("Could not find a '.'");
let i = ImportantExcerpt { part: first_sentence };
}
(This annotation means an instance of ImportantExcerpt can’t outlive the
reference it holds in its part field.)
Lifetime Elision
Lifetimes on function or method parameters are called input lifetimes, and
lifetimes on return values are called output lifetimes.
The compiler uses three rules to figure out what lifetimes references have when
there aren’t explicit annotations. The first one is that each parameter that is
a reference gets its own lifetime parameter. The second one is if there is exactly
one input lifetime parameter, that lifetime is assigned to all output lifetime
parameters. The third one is if there are multiple input lifetime parameters, but
one of them is &self
or &mut self
because this is a method, the lifetime ofself
is assigned to all output lifetime parameters.
Lifetime Annotations in Method Definitions
1 | impl<'a> ImportantExcerpt<'a> { |
(The return type gets the lifetime of &self)
The Static Lifetime
'static
lifetime means that this reference can live for the entire duration of
the program. All string literals have the 'static
lifetime.
1 | let s: &'static str = "I have a static lifetime."; |
Generic Type Parameters, Trait Bounds, and Lifetimes Together
1 | use std::fmt::Display; |
11. Writing Automated Tests
How to Write Tests
There are some features can be used for writing tests, include the test
attribute,
a few macros, and the should_panic
attribute.
The Anatomy of a Test Function
At its simplest, a test in Rust is a function that’s annotated with the test
attribute. Add #[test]
on the line before fn
to change a function into a test
function. When you run your tests with the cargo test
command, Rust builds a
test runner binary that runs the functions annotated with the test attribute
and reports on whether each test function passes or fails.
When we make a new library project with Cargo, a test module with a test function in it is automatically generated for us.1
cargo new adder --lib
auto generated file src/lib.rs:1
2
3
4
5
6
7
mod tests {
fn it_works() {
assert_eq!(2 + 2, 4);
}
}
Checking Results with the assert!
Macro
1 |
|
Testing Equality with the assert_eq!
and assert_ne!
Macros
1 | pub fn add_two(a: i32) -> i32 { |
All the primitive types and most of the standard library types implement PartialEq
and Debug
traits. Both two traits are derivable, so we can add#[derive(PartialEq, Debug)]
annotation to your struct or enum definition for
comparing in asset_eq!
or assert_ne!
.
Adding Custom Failure Messages
We can also add a custom message to be printed with the failure message as
optional arguments to the assert!
, assert_eq!
, and assert_ne!
macros.
1 | pub fn greeting(name: &str) -> String { |
Checking for Panics with should_panic
Adding another attribute, should_panic
, to our test function. This attribute
makes a test pass if the code inside the function panics.
1 | pub struct Guess { |
To make should_panic
tests more precise, we can add an optional expected
parameter to the should_panic attribute.
1 | pub struct Guess { |
Using Result<T, E>
in Tests
1 |
|
Controlling How Tests Are Run
Some command line options go to cargo test
, and some go to the resulting test
binary. To separate these two types of arguments, you list the arguments that go
to cargo test followed by the separator --
and then the ones that go to the
test binary.1
2cargo test --help
cargo test -- --help
Running Tests in Parallel or Consecutively
When you run multiple tests, by default they run in parallel using threads.
Specify the number of threads you want to use to the test binary:1
cargo test -- --test-threads=1
Showing Function Output
If we want to see printed values for passing tests as well, we can disable the
output capture behavior by using the nocapture
flag:1
cargo test -- --nocapture
Running a Subset of Tests by Name
1 | pub fn add_two(a: i32) -> i32 { |
running single tests:1
cargo test one_hundred
filtering to run multiple tests:1
cargo test add
(This command ran all tests with add in the name.)
We can run all the tests in a module by filtering on the module’s name.
Ignoring Some Tests Unless Specifically Requested
Using the ignore
attribute to exclude tests:1
2
3
4
5
6
7
8
9
10
fn it_works() {
assert_eq!(2 + 2, 4);
}
fn expensive_test() {
// code that takes an hour to run
}
If we want to run only the ignored tests, we can use1
cargo test -- --ignored
Test Organization
Two main test categories: unit tests and integration tests in Rust.
Unit tests are small and more focused, testing one module in isolation at a
time, and can test private interfaces. Integration tests are entirely external
to your library and use your code in the same way any other external code would,
using only the public interface and potentially exercising multiple modules per test.
Unit Tests
You’ll put unit tests in the src directory in each file with the code that
they’re testing. The convention is to create a module named tests in each file
to contain the test functions and to annotate the module with cfg(test)
.
The Tests Module and #[cfg(test)]
The #[cfg(test)]
annotation on the tests module tells Rust to compile and run
the test code only when you run cargo test
, not when you run cargo build
.
Because integration tests go in a different directory, they don’t need the#[cfg(test)]
annotation.
Testing Private Functions
1 | pub fn add_two(a: i32) -> i32 { |
Integration Tests
In Rust, integration tests are entirely external to your library.They use your
library in the same way any other code would, which means they can only call
functions that are part of your library’s public API.
To create integration tests, you first need a tests directory.
The tests Directory
Cargo will compile each of the files in the tests directory as an individual crate.
tests/integration_test.rs:1
2
3
4
5
6use adder; // bring our library (i.e. adder) into this test crate's scope
fn it_adds_two() {
assert_eq!(4, adder::add_two(2));
}
We don’t need to annotate any code in the files within the tests directory with#[cfg(test)]
.
We can still run a particular integration test function by specifying the test
function’s name as an argument to cargo test
. To run all the tests in a
particular integration test file, use the --test
argument of cargo test
followed by the name of the file.
1 | cargo test --test integration_test |
Submodules in Integration Tests
Files in subdirectories of the tests directory don’t get compiled as separate
crates or have sections in the test output.
tests/common/mod.rs:1
2
3pub fn setup() {
// snip
}
tests/integration_test.rs:1
2
3
4
5
6
7
8
9use adder;
mod common;
fn it_adds_two() {
common::setup();
assert_eq!(4, adder::add_two(2));
}
Integration Tests for Binary Crates
If our project is a binary crate that only contains a src/main.rs file and
doesn’t have a src/lib.rs file, we can’t create integration tests in the
tests directory and bring functions defined in the src/main.rs file into
scope with a use
statement. Only library crates expose functions that other
crates can use; binary crates are meant to be run on their own.
This is one of the reasons Rust projects that provide a binary have a
straightforward src/main.rs file that calls logic that lives in the
src/lib.rs file. Using that structure, integration tests can test the library
crate with use
to make the important functionality available.