Rust Command Line Application
Rust is a strongly typed programming language with focus on performance and memory-safety. It was designed at Mozilla Labs in 2010.
Further Material
- Homepage: rust-lang.org
- The Rust Book: doc.rust-lang.org
- Rust Package Registry: crates.io
- Cargo Book: doc.rust-lang.org
Topics, Tools and Terms
Rust packages are called crates, and the tool to manage the lifecycle of a project is called cargo. It comes with the Rust language distribution itself.
Cargo lets us run commands to build and test projects or install new binaries.
It does not provide a way via the command line to add a new dependency, though.
There is, however, a tool available for that: cargo-edit
(see cargo-edit
’s GitHub repository).
Dependency Management
The way to add new packages to a project is to edit a file called Cargo.toml
and add the new package(s) in there.
Whenever the next cargo build
or cargo test
is executed, cargo will download and compile any new dependencies then.
Cargo uses the file Cargo.toml
to keep track of required dependencies for a given project (together with Cargo.lock
).
An example Cargo.toml
looks like this:
Version Managers
The official tool to manage different versions of Rust on one’s system is called rustup. See its website as well as its GitHub repository for more details.
Testing Tools
The Rust programming language comes with testing support built in. It provides some basic helper macros for assertions:
assert!(expression)
assert_eq!(left, right)
assert_ne!(left, right)
When first looking at only these three primitives it seemed as if that wasn’t enough for a good suite of (unit) tests. It turns out that testing with just these three macros, projects can get a long way until they feel the need for more sophisticated testing capabilities.
There are some projects out there that attempt to bring Hamcrest matchers to Rust, though. In our guide we will be using the default testing macros Rust comes with only.
Directory Structure
Cargo is used to scaffold a new command line project for us.
It refers to it as a binary project and can be created via cargo new <project name> --bin
.
The good thing about cargo taking care of a project layout is that it contains the community standards of what a Rust project should look like.
In Rust there are technically no discussions whether or not source code should live in src
or lib
or something else.
The tooling takes care of that, and the tooling has been created with and by the community of Rust.
A typical directory structure consists of a src
directory that contains all source modules.
We provided a working example of a minimal project on Github.
- src
- Cargo.toml
- Cargo.lock
The Cargo Book has its own chapter about package layout with further details.
Unit Tests and Integration Tests
One difference that stands out to other languages is that there is no src
/test
directory separation between production code and its unit tests in Rust.
Unit tests live inside the modules defined in src
— usually towards the bottom of the file.
It is possible, though, to have a tests
directory on the root level of our projects.
Tests in there are considered integration tests, that test a wider scope of the project than individual unit tests.
The Rust Book has more guidance on writing tests and test organisation.
Naming Conventions
File and directory names are in lower case (snake case if separations are needed).
There’s no 1:1 relation between functions, traits or structs and their containing modules.
For example the trait Example
does not have to be defined in a file called example.rs
.
Example Project
The repository for the example applications is available at github.com/vanilla-project/rust-command-line.
The main application consists of two files:
src/main.rs
is the main executable with a function that uses:src/example.rs
which contains only one function that returns a string.
Running the Application
To run the application we can use cargo. After it has been compiled, this should print the text “Rust Example”.
$: cargo run
Compiling rust-command-line v0.1.0 (/home/vanilla/rust-command-line)
Finished dev [unoptimized + debuginfo] target(s) in 1.90s
Running `target/debug/rust-command-line`
Rust Example
Running the Tests
To run the tests we execute cargo test
which then looks for all modules inside of src
that contain test code and executes them.
The output should look like the following:
$: cargo test
Finished test [unoptimized + debuginfo] target(s) in 0.00s
Running target/debug/deps/rust_command_line-3c93c33fdb784beb
running 2 tests
test example::tests::returns_message ... ok
test tests::prints_message ... ok
test result: ok. 2 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out
Testing Approach
The test for function example::message
is only verifying the return value of it.
Testing main::print_messaage
on the other hand is done via a test-double that gets injected.
This allows us to spy on the output it produces.
We want to avoid printing anything to the screen while running the tests.
Injecting a test double in this instance is a nice way to isolate our application from the command line.