I don't really blog anymore. Click here to go to my main website.

muhuk's blog

Nature, to Be Commanded, Must Be Obeyed

April 03, 2018

Getting a Little Further Than Hello World With Rust - Part 2: Test Driven Development

This is the second post of Getting a Little Further Than Hello World With Rust series. We will write some Rust code with tests. In fact we will be practising Test Driven Design.

  1. We will start by writing a unit test and make sure it fails.
  2. We will write the code to make the test pass. Just enough code, nothing more.
  3. If there are any opportunities, We will refactor our code.

Rinse and repeat.

FizzBuzz

Depending on where you stand on the digital knowledge divide, FizzBuzz can be deceptively simple (actually simple but appears hard) or deceptively hard (for those people who do not have coding skills). The reason I have chosen to write a FizzBuzz implementation is simply deficiency of my imagination. However tempting I will leave ranting on how many so-called programmers fail to come up with a working FizzBuzz solution, or highlighting the growing importance of acquiring coding skills for anyone, programmer or not… I will leave these to be addressed in future posts.

For our FizzBuzz problem, the task is defined as below:

  • Write a program that outputs numbers from 1 to 100.
  • For multiples of 3 output "Fizz" instead of the actual number.
  • For multiples of 5 output "Buzz".
  • For those numbers that are multiples of both 3 and 5 output “FizzBuzz”.

Writing Tests in Rust

Test support only Rust and Cargo provides is rudimentary but quite adequte to get started. It is quite a cool things to see testing is a first class activity for the language.

Let us start by creating our test block:

#[cfg(test)]         // Compile `tests` module only during testing.
mod tests {
    use super::*;    // Import all members of the containing
}                    // module, so we can directly call them.

Then we add our first test:

#[cfg(test)]
mod tests {
    use super::*;

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

Running this would result in a compilation error of cannot find function `fizzbuzz` and a warning of unused import: `super::*;` because we have not created fizzbuzz function yet.

pub fn fizzbuzz(x: usize) -> String {
    return x.to_string();
}

Now our first test should pass:

running 1 test
test tests::test_fizzbuzz_1 ... ok

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

If we add the test of assert_eq!(fizzbuzz(2), "2"); it should also pass. However the test of assert!(fizzbuzz(3) != "Fizz"); will fail. fizzbuzz simply converts the input to a string at this stage:

pub fn fizzbuzz(x: usize) -> String {
    return x.to_string();
}


#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn test_fizzbuzz_1() {
        assert_eq!(fizzbuzz(1), "1");    // Pass
    }

    #[test]
    fn test_fizzbuzz_2() {
        assert_eq!(fizzbuzz(2), "2");    // Pass
    }

    #[test]
    fn test_fizzbuzz_3() {
        assert!(fizzbuzz(3) != "Fizz");  // Fail
    }
}

Before we address the failing third test, let us recap which TDD practises we followed so far. Before we started writing production code we have written tests. We have run our tests first to ensure they failed. Then we have written just enough production code to pass the test. Now we will do the same again:

pub fn fizzbuzz(x: usize) -> String {
    if x == 3 {
        return String::from("Fizz");
    } else {
        return x.to_string();
    }
}

We know the result is "Fizz" not just for 3, but for all multiples of 3. But so far we only have one test that covers the input 3. Writing the code intelligent enough to handle all multiples of 3 would be overspecifying it. When practising TDD we want to avoid such smart code because we do not have the tests to verify the behavior. Better keep things simple and have the confidence from our coverage. It is better to have this overly simplistic code daring us to refactor. And refactor it we will.

Let us add a test case for input 4. Of course we will not be adding one test for every single natural number. That would be impractical. We should be able to skip a few numbers and also we should be able to stop at a certain number of tests. But we should do this when we are confident enough with the existing set of tests and the state of our code. It is better to err on the side of too many tests.

Anyway when we add the test for 4 it would pass as expected. But the next number, 5 would fail because our function is returning "5" and not "Buzz":

#[test]
fn test_fizzbuzz_4() {
    assert_eq!(fizzbuzz(4), "4");
}

#[test]
fn test_fizzbuzz_5() {
    assert_eq!(fizzbuzz(5), "Buzz");
}

Let us fix that by adding a condition for 5:

#[allow(dead_code)]
fn fizzbuzz(x: usize) -> String {
    if x == 3 {
        return String::from("Fizz");
    }
    else if x == 5 {
        return String::from("Buzz");
    }
    else {
        return x.to_string();
    }
}

Now all our tests pass again. But our fizzbuzz function is not quite up to the spec. I will not go into the details of what is happening below too much, but we would refactor once we have added test case for 6 and made it pass. Then once again for 10 and then finally we would need to add a new condition for 15. If we are following TDD to the letter, we would first write the simplistic code that made the test pass and only then refactor. I present only the resulting code here:

pub fn fizzbuzz(x: usize) -> String {
    if x % 15 == 0 {
        return String::from("FizzBuzz");
    } else if x % 3 == 0 {
        return String::from("Fizz");
    } else if x % 5 == 0 {
        return String::from("Buzz");
    } else {
        return x.to_string();
    }
}


#[cfg(test)]
mod tests {
    use super::*;

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

    #[test]
    fn test_fizzbuzz_2() {
        assert_eq!(fizzbuzz(2), "2");
    }

    #[test]
    fn test_fizzbuzz_3() {
        assert_eq!(fizzbuzz(3), "Fizz");
    }

    #[test]
    fn test_fizzbuzz_4() {
        assert_eq!(fizzbuzz(4), "4");
    }

    #[test]
    fn test_fizzbuzz_5() {
        assert_eq!(fizzbuzz(5), "Buzz");
    }

    #[test]
    fn test_fizzbuzz_6() {
        assert_eq!(fizzbuzz(6), "Fizz");
    }

    #[test]
    fn test_fizzbuzz_7() {
        assert_eq!(fizzbuzz(7), "7");
    }

    #[test]
    fn test_fizzbuzz_8() {
        assert_eq!(fizzbuzz(8), "8");
    }

    #[test]
    fn test_fizzbuzz_9() {
        assert_eq!(fizzbuzz(9), "Fizz");
    }

    #[test]
    fn test_fizzbuzz_10() {
        assert_eq!(fizzbuzz(10), "Buzz");
    }

    #[test]
    fn test_fizzbuzz_11() {
        assert_eq!(fizzbuzz(11), "11");
    }

    #[test]
    fn test_fizzbuzz_12() {
        assert_eq!(fizzbuzz(12), "Fizz");
    }

    #[test]
    fn test_fizzbuzz_13() {
        assert_eq!(fizzbuzz(13), "13");
    }

    #[test]
    fn test_fizzbuzz_14() {
        assert_eq!(fizzbuzz(14), "14");
    }

    #[test]
    fn test_fizzbuzz_15() {
        assert_eq!(fizzbuzz(15), "FizzBuzz");
    }
}

We will stop testing numbers at 15 for the purposes of this post, but normally I would add more tests.

Next we will create an iterator for fizzbuzz. It will be a simple one because fizzbuzz results do not depend on previous results (unlike fibonacci series) and all the state that we need to keep is an integer.

Iterators

First let us remove pub from fizzbuzz. Our existing tests will continue to work but we want to expose fizzbuzz_iter_stateful as our public API:

// pub fn fizzbuzz_iter_stateful() -> FizzBuzzIterator


fn fizzbuzz(x: usize) -> String {  // Not public anymore.
    if x % 15 == 0 {
        return String::from("FizzBuzz");
    } else if x % 3 == 0 {
        return String::from("Fizz");
    } else if x % 5 == 0 {
        return String::from("Buzz");
    } else {
        return x.to_string();
    }
}

Let us add a new test case for the iterator. Here I am taking a bit of a shortcut collecting a vector of 15 elements out of the iterator at once. I think it would be a valid argument to build up tests for the iterator gradually. But also consider the argument against testing the iterator interface every single time we implement an iterator.

#[test]
fn test_fizzbuzz_iter_stateful() {
    assert_eq!(
        fizzbuzz_iter_stateful().take(15).collect::<Vec<_>>(),
        vec![
            "1",
            "2",
            "Fizz",
            "4",
            "Buzz",
            "Fizz",
            "7",
            "8",
            "Fizz",
            "Buzz",
            "11",
            "Fizz",
            "13",
            "14",
            "FizzBuzz",
        ]
    );
}

Here is the implementation of fizzbuzz_iter_stateful and FizzBuzzIterator:

use std::iter::Iterator;


pub struct FizzBuzzIterator {
    n: usize,
}

impl Iterator for FizzBuzzIterator {
    type Item = String;

    fn next(&mut self) -> Option<Self::Item> {
        let result = Some(fizzbuzz(self.n));
        self.n += 1;
        return result;
    }
}

pub fn fizzbuzz_iter_stateful() -> FizzBuzzIterator {
    return FizzBuzzIterator { n: 1 };
}

Notice that Rust keeps user defined data types and operations that can be performed on them separate, unline object oriented languages. As a result Rust also does not have object inheritance. If you squint your eyes enough the code above looks a lot like Haskell.

Writing functional code in Rust is a lot easier than other not strictly functional languages. For example fizzbuzz_iter_stateful().take(15).collect::<Vec<_>>().

But of course next mutates the object it is called on (&mut self) and that is not functional. We will try making our code more functional below. Here is the full listing of iterator version:

use std::iter::Iterator;


pub struct FizzBuzzIterator {
    n: usize,
}

impl Iterator for FizzBuzzIterator {
    type Item = String;

    fn next(&mut self) -> Option<Self::Item> {
        let result = Some(fizzbuzz(self.n));
        self.n += 1;
        return result;
    }
}

pub fn fizzbuzz_iter_stateful() -> FizzBuzzIterator {
    return FizzBuzzIterator { n: 1 };
}


fn fizzbuzz(x: usize) -> String {
    if x % 15 == 0 {
        return String::from("FizzBuzz");
    } else if x % 3 == 0 {
        return String::from("Fizz");
    } else if x % 5 == 0 {
        return String::from("Buzz");
    } else {
        return x.to_string();
    }
}


#[cfg(test)]
mod tests {
    use super::*;

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

    #[test]
    fn test_fizzbuzz_2() {
        assert_eq!(fizzbuzz(2), "2");
    }

    #[test]
    fn test_fizzbuzz_3() {
        assert_eq!(fizzbuzz(3), "Fizz");
    }

    #[test]
    fn test_fizzbuzz_4() {
        assert_eq!(fizzbuzz(4), "4");
    }

    #[test]
    fn test_fizzbuzz_5() {
        assert_eq!(fizzbuzz(5), "Buzz");
    }

    #[test]
    fn test_fizzbuzz_6() {
        assert_eq!(fizzbuzz(6), "Fizz");
    }

    #[test]
    fn test_fizzbuzz_7() {
        assert_eq!(fizzbuzz(7), "7");
    }

    #[test]
    fn test_fizzbuzz_8() {
        assert_eq!(fizzbuzz(8), "8");
    }

    #[test]
    fn test_fizzbuzz_9() {
        assert_eq!(fizzbuzz(9), "Fizz");
    }

    #[test]
    fn test_fizzbuzz_10() {
        assert_eq!(fizzbuzz(10), "Buzz");
    }

    #[test]
    fn test_fizzbuzz_11() {
        assert_eq!(fizzbuzz(11), "11");
    }

    #[test]
    fn test_fizzbuzz_12() {
        assert_eq!(fizzbuzz(12), "Fizz");
    }

    #[test]
    fn test_fizzbuzz_13() {
        assert_eq!(fizzbuzz(13), "13");
    }

    #[test]
    fn test_fizzbuzz_14() {
        assert_eq!(fizzbuzz(14), "14");
    }

    #[test]
    fn test_fizzbuzz_15() {
        assert_eq!(fizzbuzz(15), "FizzBuzz");
    }

    #[test]
    fn test_fizzbuzz_iter_stateful() {
        assert_eq!(
            fizzbuzz_iter_stateful().take(15).collect::<Vec<_>>(),
            vec![
                "1",
                "2",
                "Fizz",
                "4",
                "Buzz",
                "Fizz",
                "7",
                "8",
                "Fizz",
                "Buzz",
                "11",
                "Fizz",
                "13",
                "14",
                "FizzBuzz",
            ]
        );
    }
}

Functional Style

As the last step of this post we will write a functional version of fizzbuzz. We will build an immutable, infinite stream of fizzbuzz results.

Our stream will have two functions implemented on it; head to get the first item in the stream, if there are any, and tail to get a copy of the stream with the first item (if any) discarded. This interface may not be too great for general purpose streams but since a fizzbuzz is defined on all natural numbers we know that our stream will never terminate[1]. So it should be okay for our use case. Before we start writing the production code let us write some tests:

#[test]
fn test_fizzbuzz_functional() {
    assert_eq!(fizzbuzz_functional().head().unwrap(), "1");
    assert_eq!(fizzbuzz_functional().tail().head().unwrap(), "2");
    assert_eq!(fizzbuzz_functional().tail().tail().head().unwrap(), "Fizz");
    assert_eq!(
        fizzbuzz_functional().tail().tail().tail().head().unwrap(),
        "4"
    );
    assert_eq!(
        fizzbuzz_functional()
            .tail()
            .tail()
            .tail()
            .tail()
            .head()
            .unwrap(),
        "Buzz"
    );
}

I have added a single test with five assertions here for the sake of keeping it short. But ideally each assertion should have its own test case. There are two reasons for this:

  1. First assertion features only head and head can be built & tested before we start tail. Remember the second rule of TDD? You are allowed to write as much unit tests as it is sufficient to fail and no more. If my first test passes, and any subsequent test that makes use of tail would fail then I would know to first look at tail’s implementation.

  2. Fundamentally a unit test needs to be atomic (small, indivisible). This one large test can obviously be divided into smaller tests. Note that this does not necessarily mean one assertion per one test case. Sometimes multiple facets of a result needs to be checked and it is better to group them in a single test:

    let result = create_fancy_car();
    assert!(result.is_nice());
    assert!(result.is_shiny());
    

In these posts we are getting a little further than hello world but not really going deep all the way down the rabbit hole. I just wanted to call out this instance of cutting corners and give my rationale why multiple test cases are warranted.

Moving on… We will use the following data structure to encapsulate our stream:

pub struct FizzBuzzCons {
    n: usize,
}

Note that it is exactly same as FizzBuzzIterator. We will be using FizzBuzzCons as an immutable data structure. Rust does not allow marking data structures or individual fields as read-only. Instead you specify mutability in function signatures and lexical assignments. See head & tail below uses a &self reference and not a &mut self like Iterator.next:

trait Cons {
    type Item;

    fn head(&self) -> Option<Self::Item>;

    fn tail(&self) -> Box<Cons<Item = Self::Item>>;
}


impl Cons for FizzBuzzCons {
    type Item = String;

    fn head(&self) -> Option<String> {
        return Some(fizzbuzz(self.n));
    }

    fn tail(&self) -> Box<Cons<Item = String>> {
        return Box::new(FizzBuzzCons { n: self.n + 1 });
    }
}

As &self is not mutable so self.n is also not mutable. If you are familiar with functional programming, above code should be easy to grok. I will try to explain anyway. Both head and tail return immutable values. You can read a FizzBuzzCons’s n but you cannot change it. tail builds an entirely new instance of FizzBuzzCons with n incremented. The advantage of this style are:

  1. Immutable objects can be safely shared across threads. There is no possibility of two threads modifying an object if the object cannot be modified.
  2. If head and tail are implemented correctly, the stream cannot be in an inconsistent state. To generalize this; an immutable data structure provides safe access to high level operations given a small number of primitive operations defined on it are correct.

And small number of primitive operations, namely head and tail here, are all we need. I have not decided what to cover in the third part of these series, let me know what you would like to read about.

Below is the full source code of what we have covered in this post:

use std::iter::Iterator;


// Iterator

pub struct FizzBuzzIterator {
    n: usize,
}

impl Iterator for FizzBuzzIterator {
    type Item = String;

    fn next(&mut self) -> Option<Self::Item> {
        let result = Some(fizzbuzz(self.n));
        self.n += 1;
        return result;
    }
}

pub fn fizzbuzz_iter_stateful() -> FizzBuzzIterator {
    return FizzBuzzIterator { n: 1 };
}


// Functional

trait Cons {
    type Item;

    fn head(&self) -> Option<Self::Item>;

    fn tail(&self) -> Box<Cons<Item = Self::Item>>;
}

pub struct FizzBuzzCons {
    n: usize,
}

impl Cons for FizzBuzzCons {
    type Item = String;

    fn head(&self) -> Option<String> {
        return Some(fizzbuzz(self.n));
    }

    fn tail(&self) -> Box<Cons<Item = String>> {
        return Box::new(FizzBuzzCons { n: self.n + 1 });
    }
}

pub fn fizzbuzz_functional() -> FizzBuzzCons {
    return FizzBuzzCons { n: 1 };
}


// FizzBuzz

fn fizzbuzz(x: usize) -> String {
    if x % 15 == 0 {
        return String::from("FizzBuzz");
    } else if x % 3 == 0 {
        return String::from("Fizz");
    } else if x % 5 == 0 {
        return String::from("Buzz");
    } else {
        return x.to_string();
    }
}


#[cfg(test)]
mod tests {
    use super::*;

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

    #[test]
    fn test_fizzbuzz_2() {
        assert_eq!(fizzbuzz(2), "2");
    }

    #[test]
    fn test_fizzbuzz_3() {
        assert_eq!(fizzbuzz(3), "Fizz");
    }

    #[test]
    fn test_fizzbuzz_4() {
        assert_eq!(fizzbuzz(4), "4");
    }

    #[test]
    fn test_fizzbuzz_5() {
        assert_eq!(fizzbuzz(5), "Buzz");
    }

    #[test]
    fn test_fizzbuzz_6() {
        assert_eq!(fizzbuzz(6), "Fizz");
    }

    #[test]
    fn test_fizzbuzz_7() {
        assert_eq!(fizzbuzz(7), "7");
    }

    #[test]
    fn test_fizzbuzz_8() {
        assert_eq!(fizzbuzz(8), "8");
    }

    #[test]
    fn test_fizzbuzz_9() {
        assert_eq!(fizzbuzz(9), "Fizz");
    }

    #[test]
    fn test_fizzbuzz_10() {
        assert_eq!(fizzbuzz(10), "Buzz");
    }

    #[test]
    fn test_fizzbuzz_11() {
        assert_eq!(fizzbuzz(11), "11");
    }

    #[test]
    fn test_fizzbuzz_12() {
        assert_eq!(fizzbuzz(12), "Fizz");
    }

    #[test]
    fn test_fizzbuzz_13() {
        assert_eq!(fizzbuzz(13), "13");
    }

    #[test]
    fn test_fizzbuzz_14() {
        assert_eq!(fizzbuzz(14), "14");
    }

    #[test]
    fn test_fizzbuzz_15() {
        assert_eq!(fizzbuzz(15), "FizzBuzz");
    }

    #[test]
    fn test_fizzbuzz_iter_stateful() {
        assert_eq!(
            fizzbuzz_iter_stateful().take(15).collect::<Vec<_>>(),
            vec![
                "1",
                "2",
                "Fizz",
                "4",
                "Buzz",
                "Fizz",
                "7",
                "8",
                "Fizz",
                "Buzz",
                "11",
                "Fizz",
                "13",
                "14",
                "FizzBuzz",
            ]
        );
    }

    #[test]
    fn test_fizzbuzz_functional() {
        assert_eq!(fizzbuzz_functional().head().unwrap(), "1");
        assert_eq!(fizzbuzz_functional().tail().head().unwrap(), "2");
        assert_eq!(fizzbuzz_functional().tail().tail().head().unwrap(), "Fizz");
        assert_eq!(
            fizzbuzz_functional().tail().tail().tail().head().unwrap(),
            "4"
        );
        assert_eq!(
            fizzbuzz_functional()
                .tail()
                .tail()
                .tail()
                .tail()
                .head()
                .unwrap(),
            "Buzz"
        );
    }
}
[1]But that does not mean our usize will not overflow.

If you have any questions, suggestions or corrections feel free to drop me a line.