Hello World
Start new project
To start a new project, we use the cargo new command. This will generate everything we need to get our program running.
cargo new hello_world
The structure of the new directory looks like this:
hello_world
├── Cargo.toml
└── src
└── main.rs
With:
hello_worldas our project name.Cargo.tomlis the file that manages our project.srcis the directory all our code goes in.main.rsis the file that holds our code.
If we open main.rs, we will see the traditional "Hello, World!" code.
fn main() { println!("Hello, World!"); }
Every program requires a main() function to run, since this is where every program we write begins at.
To run our code, we can type cargo run in our terminal.
This will:
- Build our code into a binary file.
- Run our code after the build is finished.
Once cargo run finishes running, we should see "Hello, World!" in our terminal.
Hello, World!
Every cargo new project starts with "Hello, World!" using the println! macro.
fn main() { println!("Hello, world!"); let hello = hello_world(); println!("{}", hello); let name = String::from("Rusty"); greeting(name); } fn hello_world() -> String { let greeting = String::from("Hello, World!"); greeting } fn greeting(name: String) -> String { let hello = String::from("Hello, "); let greeting = format!("{hello}{name}!"); greeting } #[cfg(test)] mod tests { use super::*; #[test] fn hello_world_test() { let want = String::from("Hello, World!"); let result = hello_world(); assert_eq!(want, result); } #[test] fn greeting_test() { let want = String::from("Hello, Rusty!"); let name = String::from("Rusty"); let result = greeting(name); assert_eq!(want, result); } }
However, when we write tests, we want to separate our "domain" code from the outside world (side effects). The outside world is dealing with file input/output, printing to the terminal, interfacing with databases, so on and so forth.
To be testable, we need to turn our "Hello, World!" into a function and print the result of the function to the screen separately.
First, we write the function.
fn main() { println!("Hello, world!"); let hello = hello_world(); println!("{}", hello); let name = String::from("Rusty"); greeting(name); } fn hello_world() -> String { let greeting = String::from("Hello, World!"); greeting } fn greeting(name: String) -> String { let hello = String::from("Hello, "); let greeting = format!("{hello}{name}!"); greeting } #[cfg(test)] mod tests { use super::*; #[test] fn hello_world_test() { let want = String::from("Hello, World!"); let result = hello_world(); assert_eq!(want, result); } #[test] fn greeting_test() { let want = String::from("Hello, Rusty!"); let name = String::from("Rusty"); let result = greeting(name); assert_eq!(want, result); } }
- The
fnkeyword is how we define our function. hello_worldis the name of our function.()is where would would define our arguments if we have any.-> Stringis the return type we expect.let greeting =is an immutable variable assignment.String::from("Hello, World!")creates a "Hello, World!" in theStringtype.;semi-colons denote the end of a line.greetingreturns the value. We could also writereturn greetingand it would be the same thing.
In order to run hello_world(), we have to call it from the main().
fn main() {
let hello = hello_world();
println!("{}", hello);
}
Running cargo run again builds and runs our code.
This prints "Hello, World!" to the screen the same way println!("Hello, World!") does, but is now ready to be tested.
Writing our first test
Now that we have separated the domain code from any side effects, we write our first test.
fn main() { println!("Hello, world!"); let hello = hello_world(); println!("{}", hello); let name = String::from("Rusty"); greeting(name); } fn hello_world() -> String { let greeting = String::from("Hello, World!"); greeting } fn greeting(name: String) -> String { let hello = String::from("Hello, "); let greeting = format!("{hello}{name}!"); greeting } #[cfg(test)] mod tests { use super::*; #[test] fn hello_world_test() { let want = String::from("Hello, World!"); let result = hello_world(); assert_eq!(want, result); } #[test] fn greeting_test() { let want = String::from("Hello, Rusty!"); let name = String::from("Rusty"); let result = greeting(name); assert_eq!(want, result); } }
#[cfg(test)]is a macro that says "this block of code will be our tests". This is howcargoknows which code to run when you usecargo test.mod testsis the name of the block.use super::*pulls all the code above the test block into scope.[#test]is a macro we put on each function we want to count as a test.fn hello_world_test()is our first test. Note: no return value is given, since tests aren't supposed to return anything; they're supposed to test code.let want = String::from("Hello, World!")is our desired outcome for the testlet result = hello_world()is the result we get back from the function we're testing.assert_eq!(want, result)asserts thewantandresultare the same value.
We can now run our tests with cargo test.
We should see the following output:
Compiling hello_world v0.1.0 (/Users/glitch/projects/learn_rust_with_tests/examples/01_
hello_world/hello_world)
Finished test [unoptimized + debuginfo] target(s) in 0.65s
Running unittests src/main.rs (target/debug/deps/hello_world-9e795269315caeb3)
running 1 tests
test tests::greeting_test ... ok
test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished i
n 0.00s
Taking in a name
The purpose of tests is to define a desired outcome and write it in code. By first defining what outcome we wish to achieve, we can iterate on our code to match the test.
Our new requirement is to take in a name and print that out with "Hello". For example, the outcome should be "Hello, Rusty!"
Since this is Test-Driven Development, we write our test first.
fn main() { println!("Hello, world!"); let hello = hello_world(); println!("{}", hello); let name = String::from("Rusty"); greeting(name); } fn hello_world() -> String { let greeting = String::from("Hello, World!"); greeting } fn greeting(name: String) -> String { let hello = String::from("Hello, "); let greeting = format!("{hello}{name}!"); greeting } #[cfg(test)] mod tests { use super::*; #[test] fn hello_world_test() { let want = String::from("Hello, World!"); let result = hello_world(); assert_eq!(want, result); } #[test] fn greeting_test() { let want = String::from("Hello, Rusty!"); let name = String::from("Rusty"); let result = greeting(name); assert_eq!(want, result); } }
fn greeting_test()is defined.let want = String::from("Hello, Rusty!");is the outcome we want.let name = String::from("Rusty!")is the name we pass an an argument togreeting().let result = greeting(name)callsgreetingwith ournamevariable and saves the return value toresult.assert_eq!(want, result)assertswantandresultare the same.
If we run cargo test now, we will get an error:
$ cargo test
Compiling hello_world v0.1.0 (/Users/rt/projects/learn_rust_with_tests/examples/01_
hello_world/hello_world)
error[E0425]: cannot find function `greeting` in this scope
--> src/main.rs:46:22
|
46 | let result = greeting(name);
| ^^^^^^^^ not found in this scope
For more information about this error, try `rustc --explain E0425`.
error: could not compile `hello_world` (bin "hello_world" test) due to 1 previous erro
r
That's because greeting() doesnt exist yet.
We can create a placeholder using the unimplemented!() macro. When cargo test is ran, it wont fail:
#![allow(unused)] fn main() { fn greeting() { unimplemented!() } }
Now we need to write the code to fulfill our test.
fn main() { println!("Hello, world!"); let hello = hello_world(); println!("{}", hello); let name = String::from("Rusty"); greeting(name); } fn hello_world() -> String { let greeting = String::from("Hello, World!"); greeting } fn greeting(name: String) -> String { let hello = String::from("Hello, "); let greeting = format!("{hello}{name}!"); greeting } #[cfg(test)] mod tests { use super::*; #[test] fn hello_world_test() { let want = String::from("Hello, World!"); let result = hello_world(); assert_eq!(want, result); } #[test] fn greeting_test() { let want = String::from("Hello, Rusty!"); let name = String::from("Rusty"); let result = greeting(name); assert_eq!(want, result); } }
fn greeting(name: String) -> Stringcreates functiongreeting()that takes aStringcallednameas an argument. The return type isString.let hello = String::from("Hello,");creates theString"Hello,".let greeting = format!("{hello} {name}!")formats the value ofhelloand ournameargument into a singleStringusing theformat!()macro.
We've written our function to fulfill the requirements we set in our test, so now we try running cargo test.
Running cargo test will give us "passed" on both tests.
$ cargo test
Finished test [unoptimized + debuginfo] target(s) in 0.05s
Running unittests src/main.rs (target/debug/deps/hello_world-9e795269315caeb3)
running 2 tests
test tests::greeting_test ... ok
test tests::hello_world_test ... ok
test result: ok. 2 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s
Starting in the next chapter, we will start writing our tests first, fulfilling them, and refactoring as necessary.