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

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:

[package]
name = "vanilla-project"
version = "0.1.0"
authors = ["Your Name <you@example.com>"]
edition = "2018"

[dependencies]

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.