DEV Community

Cover image for I'm a very beginner of Rustaceans. Let's write FizzBuzz
r_tanaka
r_tanaka

Posted on

I'm a very beginner of Rustaceans. Let's write FizzBuzz

I'm a very beginner of Rustanceans(rust programmer).
This article is for my understanding how to write rust.
Let's write a fizzbuzz with simple tdd process in r/rust

ToC

Introduction

What is FizzBuzz?

  1. display numbers from 1 to 100
  2. display "Fizz" when numbers are multiples of 3
  3. display "Buzz" when numbers are multiples of 5
  4. display "FizzBuzz" when numbers are multiples of 15
1, 2, Fizz, 4, Buzz, Fizz, 7 ......14, FizzBuzz, 16, 17...100

So, I try to make command fizzbazz like below.

$ fizzbazz 1
1

$ fizzbazz 3
1, 2, Fizz

$ fizzbazz 15
1, 2, Fizz, 4, Buzz, Fizz, 7, 8, Fizz, Buzz, 11, Fizz, 13, 14, FizzBuzz

What is TDD?

Test Driven Development.
You write test first, then you see testing error. (I like to say it is "see red")
You modify your code, then you see testing success. (I like to say it is "see green")

Rust Versions

$ rustc --version
rustc 1.40.0 (73528e339 2019-12-16)
$ rustup --version
rustup 1.21.1 (7832b2ebe 2019-12-20)
$ cargo --version
cargo 1.40.0 (bc8e4c8be 2019-11-22)

Create project

$ cd $rust_projects
$ cargo new fizzbuzz
$ cd fizzbuzz

Unit Test: Numbers

First test

The cargo new command generated "hello world".
Let's add test code at bottom of main.rs as below.

src/main.rs

fn main() {
    println!("Hello, world!");
}

#[cfg(test)]
mod tests {

    use super::*;

    #[test]
    fn fizzbuzz_numbers() {
        assert_eq!(1, fizzbuzz(1));
    }
}

Let's confirm it.

$ cargo run
    Finished dev [unoptimized + debuginfo] target(s) in 0.00s
     Running `target/debug/fizzbuzz`
Hello, world!

$ cargo test
   Compiling fizzbuzz v0.1.0 (/home/ryo/workspace/rust/fizzbuzz)
error[E0425]: cannot find function `fizzbuzz` in this scope
 --> src/main.rs:9:23
  |
9 |         assert_eq!(1, fizzbuzz(1))
  |                       ^^^^^^^^ not found in this scope

error: aborting due to previous error

# --snip--

The cargo run command compiles and runs the rust code.
The cargo test command compiles and runs the test code.

Let's implement fizzbuzz function.

Add fn fizzbuzz as below.

fn main() {
    println!("Hello, world!");
}

pub fn fizzbuzz(n: u32) -> u32 {
    n
}

// --snip--

Run cargo test again you can see green.

Update unit test

Update test code as below

// --snip--

    #[test]
    fn fizzbuzz_numbers() {
        for n in 1..101 {
            assert_eq!(n, fizzbuzz(n));
        }
    }

// --snip--

Unit Test: FizzBuzz

let's see red by updating feature.
update test code as below.

    #[test]
    fn fizzbuzz_multipe_15() {
        for n in 1..11 {
            assert_eq!("FizzBuzz", fizzbuzz(n*15));
        }
    }

you can see red.

$ cargo test
   Compiling fizzbuzz v0.1.0 (/home/ryo/workspace/rust/fizzbuzz)
error[E0277]: can't compare `&str` with `u32`
  --> src/main.rs:24:13
   |
24 |             assert_eq!("FizzBuzz", fizzbuzz(n*15));
   |             ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ no implementation for `&str == u32`
   |

let's make it green by updating fn fizzbazz as below

// --snip--

pub fn fizzbuzz(n: u32) -> String {
    if n%15 == 0 {
        String::from("FizzBuzz")
    } else {
        n.to_string()
    }
}

// --snip--

But still you see red because of test fizzbuzz_numbers.
udpate it as below.

    #[test]
    fn fizzbuzz_numbers() {
        for n in 1..101 {
            if n%15 == 0 {
                assert_ne!(n.to_string(), fizzbuzz(n));
            } else {
                assert_eq!(n.to_string(), fizzbuzz(n));
            }
        }
    }

Unit Test: Fizz and Buzz

Add tests for the rest as below.

#[cfg(test)]
mod tests {

    use super::*;

    #[test]
    fn fizzbuzz_numbers() {
        for n in 1..101 {
            if n%15 == 0 || n%5 == 0 || n%3 == 0 {
                assert_ne!(n.to_string(), fizzbuzz(n));
            } else {
                assert_eq!(n.to_string(), fizzbuzz(n));
            }
        }
    }

    #[test]
    fn fizzbuzz_multipe_15() {
        for n in 1..11 {
            assert_eq!("FizzBuzz", fizzbuzz(n*15));
        }
    }

    #[test]
    fn fizzbuzz_multipe_5() {
        for n in [1, 2, 4, 5, 7].iter() {
            assert_eq!("Buzz", fizzbuzz(n*5));
        }
    }

    #[test]
    fn fizzbuzz_multipe_3() {
        for n in [1, 2, 3, 4, 6].iter() {
            assert_eq!("Fizz", fizzbuzz(n*3));
        }
    }
}

I cannot use 1..11 for multiple_5 and multiple_3 because this includes mupltipes of 15.

then, you can see red by running cargo test.

$ cargo test
    Finished test [unoptimized + debuginfo] target(s) in 0.00s
     Running target/debug/deps/fizzbuzz-11b12517e9fe40f4

running 4 tests
test tests::fizzbuzz_multipe_15 ... ok
test tests::fizzbuzz_multipe_3 ... FAILED
test tests::fizzbuzz_multipe_5 ... FAILED
test tests::fizzbuzz_numbers ... FAILED

# --snip--

let's change it to green.

pub fn fizzbuzz(n: i32) -> String {
    match n%15 {
        0 => String::from("FizzBuzz"),
        5|10 => String::from("Buzz"),
        3|6|9|12 => String::from("Fizz"),
        _ => n.to_string()
    }
}

now you can see green

$ cargo test
   Compiling fizzbuzz v0.1.0 (/home/ryo/workspace/rust/fizzbuzz)
    Finished test [unoptimized + debuginfo] target(s) in 0.36s
     Running target/debug/deps/fizzbuzz-11b12517e9fe40f4

running 4 tests
test tests::fizzbuzz_multipe_5 ... ok
test tests::fizzbuzz_multipe_3 ... ok
test tests::fizzbuzz_multipe_15 ... ok
test tests::fizzbuzz_numbers ... ok

test result: ok. 4 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out

Integration Test: Hello World!

Now you have nice function fizzbuzz, but result of cargo run is still "Hello, World!". it is time to change it.

First, let's write integration tests.
You still have "Hello, World".
Let's test it as first step.

Preparation

Create directory for integration tests.

mkdir tests
touch tests/test.rs

Open Cargo.toml then add below 3 lines

# --snip--


[dev-dependencies]
assert_cmd = "0.10"
predicates = "1"

Write first an integration test.

Open tests/test.rs then add below lines.

use std::process::Command;
use assert_cmd::prelude::*;
use predicates::prelude::*;

#[test]
fn first_test() -> Result<(), Box<std::error::Error>> {
    let mut cmd = Command::cargo_bin("fizzbuzz")?;
    cmd.assert()
        .success()
        .stdout(predicate::str::contains("Hello, world!"));
    Ok(())
}

Then run below command.

$ cargo test

After downloading several modules, you can see result of test as below

running 1 test
test first_test ... ok

test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out

Integration Test: FizzBuzz

Open tests/test.rs then change the test name from first_test to input_1.

#[test]
fn input_1() -> Result<(), Box<std::error::Error>> {
    let mut cmd = Command::cargo_bin("fizzbuzz")?;
    cmd.arg("1")
    cmd.assert();
        .success()
        .stdout(predicate::str::ends_with("1\n"));
    Ok(())
}

Of course, you can see red.
Modify src/main.rs as below.

use structopt::StructOpt;

#[derive(StructOpt)]
struct Cli {
    number: String,
}

fn main() {
    let args = Cli::from_args();
    let input: i32 = match &args.number.trim().parse() {
        Ok(input) => *input,
        Err(_) => {
            println!("fail");
            0
        }
    };
    for n in 0..input {
        println!("{}", fizzbuzz(n+1));
    };
}

And open Cargo.toml then add structopt = "0.2.10" in [dependencies] section.

# --snip--

[dependencies]
structopt = "0.2.10"

# --snip--

Run test! You can see green.

Next add another test, input_3. it will return "1, 2".
I want to add common test implementation in tests/test.rs as below.

use std::process::Command;
use assert_cmd::prelude::*;
use predicates::prelude::*;

#[test]
fn input_1() -> Result<(), Box<dyn std::error::Error>> {
    test_impl("1", "1\n")
}

#[test]
fn input_3() -> Result<(), Box<dyn std::error::Error>> {
    test_impl("2", "1, 2\n")
}

fn test_impl(input: &str, output: &str) -> Result<(), Box<dyn std::error::Error>> {
    let mut cmd = Command::cargo_bin("fizzbuzz")?;
    cmd.arg(input);
    cmd.assert()
        .success()
        .stdout(predicate::str::ends_with(output));
    Ok(())
}

Run test! You can see red.
Modify fn main.

// --snip--

fn main() {
    let args = Cli::from_args();
    let input: i32 = match &args.number.trim().parse() {
        Ok(input) => *input,
        Err(_) => {
            println!("fail");
            0
        }
    };
    for n in 0..input {
        print!("{}", fizzbuzz(n+1));
        if n+1 == input {
            println!("");
        } else {
            print!(", ");
        }
    };
}
// --snip--

Run test! You can see green.

Next, add last tests.

#[test]
fn input_15() -> Result<(), Box<dyn std::error::Error>> {
    test_impl("15", "1, 2, Fizz, 4, Buzz, Fizz, 7, 8, Fizz, Buzz, 11, Fizz, 13, 14, FizzBuzz\n")
}

#[test]
fn input_100() -> Result<(), Box<dyn std::error::Error>> {
    test_impl("100", "94, Buzz, Fizz, 97, 98, Fizz, Buzz\n")
}

#[test]
fn input_null() -> Result<(), Box<dyn std::error::Error>> {
    let mut cmd = Command::cargo_bin("fizzbuzz")?;
    cmd.assert()
        .failure()
        .stderr(predicate::str::contains("The following required arguments were not provided"));
    Ok(())
}

Run test! You can see green without any changes.

Build And Run

$ cargo build
$ ./target/debug/fizzbuzz 100
1, 2, Fizz, 4, Buzz, Fizz, 7, 8, Fizz, Buzz, 11, Fizz, 13, 14, FizzBuzz, 16, 17, Fizz, 19, Buzz, Fizz, 22, 23, Fizz, Buzz, 26, Fizz, 28, 29, FizzBuzz, 31, 32, Fizz, 34, Buzz, Fizz, 37, 38, Fizz, Buzz, 41, Fizz, 43, 44, FizzBuzz, 46, 47, Fizz, 49, Buzz, Fizz, 52, 53, Fizz, Buzz, 56, Fizz, 58, 59, FizzBuzz, 61, 62, Fizz, 64, Buzz, Fizz, 67, 68, Fizz, Buzz, 71, Fizz, 73, 74, FizzBuzz, 76, 77, Fizz, 79, Buzz, Fizz, 82, 83, Fizz, Buzz, 86, Fizz, 88, 89, FizzBuzz, 91, 92, Fizz, 94, Buzz, Fizz, 97, 98, Fizz, Buzz

Congratulations!

Cover Photo by Bill Oxford on Unsplash

Top comments (0)