In this post, we are going to do some Test-Driven Development (TDD). I find TDD very useful because the tests are the first user of my code, which means that if the tests are hard to write, then the code will be difficult to use. Many folks struggle with TDD, and unit testing in general, because they test the wrong things. Your tests should only validate observable behaviour, not the implementation. This allows you to change the implementation of your code in a safe way. Not doing this makes your tests brittle and simple code changes often means broken and failing tests.

TDD is not a silver bullet but when you’ve finished, the tests will leave you with a codebase and test suite that gives you confidence that the code will work as intended in production and help you to support changes to the implementation of the system in a safe and bug-free way.

Getting to Know the Problem

The first stage of TDD is to think about the problem. What behaviour should we test? - In other words, how are we going to use the code we write?

For this introductory post, we are going to create a simple most recently used (MRU) list. The expected behaviour can be seen in the following table:

0 1 2 3 4 5
Blue Green Red Yellow Red
Blue Green Red Yellow
Blue Green Green

We start with an empty list (column 0) and add then add Blue, Green, Red, Yellow, and Red again to the top of the list and everything else in the list drops down a position. The list has a capacity of 3, so when we add yellow in column 4, Blue drops out of the list. In column 5 we add Red again, but each item must be unique, so the Red already in the list is removed and Yellow drops down a position to allow Red to be added at the top.

From this, the basic behaviour we require is:

  • Create a list with a maximum capacity
  • Add an item to the list
  • Retrieve the current list of items

I don’t think that anyone will ever want to know the capacity of a list but we could easily add it later if needed but it’s not important to the core functionality of the list.

Retrieving the current list seems fairly simple, although looks can often be deceptive.

The complexity lies in adding an item to the list as it has a capacity. The behaviour that we need to test is:

  • A new list is empty
  • Add an item not in the list to:
    • an empty list
    • a non-empty list, not at capacity
    • a list at capacity
  • Add an item already in the list to:
    • a non-empty list, not at capacity
    • a list at capacity

I’m going to rearrange the items into a different order, so that the tests are grouped by their initial state as this makes a more logical grouping for our tests:

  • A new list is empty
  • Given an empty list
    • Add an item
  • Given a non-empty list, not at capacity
    • Add an item not in the list
    • Add an item already in the list
  • Given a list at capacity
    • Add an item not in the list
    • Add an item already in the list

We are now ready to tackle the problem by implementing the behaviour, driven by tests.

Getting Started

We are going to write our code and tests in F#, using XUnit for the tests and FsUnit for the test assertions. Don’t worry if you’ve never written any F# before, the code isn’t too hard to follow.

  1. Create an Xunit test project in your IDE/code editor of choice. Make sure to set the language as F#.

  2. Add the FsUnit.Xunit Nuget package to the project.

We are going to write the code and the tests in the same file.

Replace the code in the existing Tests.fs file with the following:

module RecentlyUsedListTests

open Xunit

// - A new list is empty
// - Given an empty list
//   - Add an item
// - Given a non-empty list, not at capacity
//   - Add an item not in the list
//   - Add an item already in the list
// - Given a list at capacity
//   - Add an item not in the list
//   - Add an item already in the list

[<Fact>]
let ``My test`` () =
    Assert.True(true)

Now run your tests. They should pass and everything should be green.

Test 1 - A new list is empty

Replace My test with the following:

module ``Given a newly created recently used list`` =

    [<Fact>]
    let ``then it should contain no items`` () =
        let sut = RecentlyUsedList(capacity = 3uy)

        Assert.Equal<List<string>>([], sut.Items)

If you’re used to writing tests in C#, you may be surprised at the ability to group tests, using modules, and at the use of real language rather than having to use special casing or seperators for the names of modules and functions. Since C# and F# projects can co-exist in the same solution, C# code can be tested in F# test projects, so you don’t have to miss out. Of course, it means that you have to learn some basic F#, but that’s a good thing isn’t it?

At the moment, the code won’t compile, so we need to implement our Recently Used List. We are going to use a sinple list of strings in this example code.

Since I want to encapsulate a mutable list, I’m going to use an F# class type. Yes, F# has a very strong object programming story!

//   - a list at capacity

type RecentlyUsedList(capacity:byte) =
    member _.Items = [string capacity]

module ``Given a newly created recently used list`` =

Items is a readonly property that returns a list containing the capacity as a string.

Run your tests. This test should fail because the test expects an empty list and we haven’t provided that.

You should always write a test that fails for an expected reason and then fix it.

Let’s fix the test by doing the simplest thing to the RecentlyUsedList that we can which is to return an empty list:

type RecentlyUsedList(capacity:byte) =
    member _.Items = []

Sadly, this doesn’t compile because we are returning an untyped (object) list rather than a list of strings. As we will see, this is only a problem for the Xunit assert and not for FsUnit. Let fix this by changing the empty list to a typed one:

type RecentlyUsedList(capacity:byte) =
    member _.Items = List.empty<string>

Run the tests and they should now all pass.

Using FsUnit for Assertions

Add a reference to the FsUnit.Xunit library:

module RecentlyUsedListTests

open Xunit
open FsUnit.Xunit

Comment out the existing assert and add use FsUnit instead:

module ``Given a newly created recently used list`` =

    [<Fact>]
    let ``then it should contain no items`` () =
        let sut = RecentlyUsedList(capacity = 3uy)

        // Assert.Equal<List<string>>([], sut.Items)
        sut.Items |> should equal List.empty<string>

On the whole, F# does not support the implicit conversion between numeric types like C# does, so we must specify the type of the data, in this case a byte, with uy.

FsUnit is similar to libraries like FluentAssertions in C# and, to my eyes, is a lot more readable than the original Xunit assertion was.

Run the tests and they should all pass.

We can make it even simpler:

module ``Given a newly created recently used list`` =

    [<Fact>]
    let ``then it should contain no items`` () =
        let sut = RecentlyUsedList(capacity = 3uy)

        // Assert.Equal<List<string>>([], sut.Items)
        // sut.Items |> should equal List.empty<string>
        sut.Items |> should be Empty

Delete the top two assertions and run the tests again. they should all pass.

We can change the RecentlyUsedList code back to an empty list if we want but we are going to implement it properly soon. If you do this, remember to run your tests and make sure that they still pass.

type RecentlyUsedList(capacity:byte) =
    member _.Items = []

Test 2 - Add an item to an empty list

Write the new test in a new module:

module ``Given an empty recently used list`` =

    [<Fact>]
    let ``when you add an item then the list contains only that item`` () =
        let sut = RecentlyUsedList(capacity = 3uy)
        
        sut.Add("Blue")
        
        sut.Items |> should equal ["Blue"]

Run your tests and this test should fail because we are returning an empty list.

Let’s start implementing the feature. The first stage is to add a mutable list. In this case, I’m going to add the C# List, which is called ResizeArray in F# because F# has it’s own list type already.

type RecentlyUsedList(capacity:byte) =
    let items = ResizeArray<string>(capacity)

    member _.Items = []

At this point, we get a compiler error as the ResizeArray doesn’t take a byte for capacity. Change the capacity type to int:

type RecentlyUsedList(capacity:int) =
    let items = ResizeArray<string>(capacity)

    member _.Items = []

Now our tests won’t compile. This is a bad thing. Roll back to the last time that the tests passed. Refactor the signature of the RecentlyUsedList capacity input parameter to be an integer (int). Run the tests again and they should pass.

Now we can add our second test again:

module ``Given an empty recently used list`` =

    [<Fact>]
    let ``when you add an item then the list contains only that item`` () =
        let sut = RecentlyUsedList(capacity = 3)
        
        sut.Add("Blue")
        
        sut.Items |> should equal ["Blue"]

Run the tests and the latest only should fail because we are returning an empty list from the RecentlyUsedList.

To implement this feature in the simplest way possible will involve a few lines of code. We could have avoided this, to a certain extent, by implementing the ResizeArray in the first test as a refactoring step once the test was passing. As you get more experienced in TDD, you’ll get better at taking small, easy to rollback steps.

The first step is to create a new member function for adding an item to the list:

type RecentlyUsedList(capacity:int) =
    let items = ResizeArray<string>(capacity)
    
    member _.Add(item:string) = items.Add item
    member _.Items = []

Next, we return the items from the internal list from the RecentlyUsedList Items property:

type RecentlyUsedList(capacity:int) =
    let items = ResizeArray<string>(capacity)
    
    member _.Add(item:string) = items.Add item
    member _.Items = items |> Seq.toList

Most C# collections are viewed by F# as a Sequence, which is equivalent to IEnumerable<T> in C#. We use a handy Seq module function to convert the items into the expected F# List type.

Run the tests and they should all pass. Success!

Test 3 - Add a new item to a non-empty list, not at capacity

Let’s add a new module and test under the existing ones:

module ``Given a non-empty recently used list that is not at capacity`` =

    [<Fact>]
    let ``when you add an item not already in list then the item is added to the start of the list`` () =
        let sut = RecentlyUsedList(capacity = 3)
        sut.Add("Blue")

        sut.Add("Green")
        
        sut.Items |> should equal ["Green";"Blue"]

Run the tests and this one should fail because the items are in the wrong order.

In an F# List, the first item is the top (head).

Let’s fix this problem with a handy function from the List module:

type RecentlyUsedList(capacity:int) =
    let items = ResizeArray<string>(capacity)

    member _.Add(item:string) = items.Add item
    member _.Items = items |> Seq.toList |> List.rev

Run your tests and they should all pass.

Now that the tests pass, let’s do some refactoring to make the next features easier to implement. Move the Add functionality to a new internal function:

type RecentlyUsedList(capacity:int) =
    let items = ResizeArray<string>(capacity)
    let add item =
        items.Add item
        
    member _.Add(item:string) = add item
    member _.Items = items |> Seq.toList |> List.rev

Run the tests and they should all pass.

Test Structure

Something that I haven’t mentioned so far is how I’m structuring the code within each test. They follow a pattern called Arrange-Act-Assert. If I add the names to the relevant sections, you’ll be able to see how this looks:

module ``Given a non-empty recently used list that is not at capacity`` =

    [<Fact>]
    let ``when you add an item not already in list then the item is added to the start of the list`` () =
        // Arrange
        let sut = RecentlyUsedList(capacity = 3)
        sut.Add("Blue")

        // Act
        sut.Add("Green")
        
        // Assert
        sut.Items |> should equal ["Green";"Blue"]

Arrange sets up the test case. Arrange is the least interesting part of the test but can constitute most of the code in the test depending upon how difficult it is to get the code into the required initial state. If this gets too large or if you are repeating the same code in multiple tests, extract the code to a function that does it for you.

Act performs the behaviour that we are testing. This should be at most a single action.

Assert verifies that the result is as we expected. This doesn’t mean that you should only have one assert statement per test but that the asserts should only verify the result of a single action.

Do not write tests that verify multiple behaviours in one test because they are more likely to break over time as you add new functionality.

It is a good habit to write the names of the sections out when you first start writing tests but once you’re experienced, they provide less value.

You may also sometimes see Arrange-Act-Assert described as equivalent to Given-When-Then but they were designed for different audiences. Given-When-Then started as a structure for building User Stories in Behaviour-Driven Design. You’ll notice that the names of the modules and tests in this post follow the Given-When-Then naming style.

Now we go back to testing behaviour.

Test 4 - Add an item already in the list

Add the following test under the existing ones in the same module as Test 3:

    [<Fact>]
    let ``when you add an item already in list then the item is moved to the start of the list`` () =
        let sut = RecentlyUsedList(capacity = 3)
        sut.Add("Blue")
        sut.Add("Green")
        
        sut.Add("Blue")
        
        sut.Items |> should equal ["Blue";"Green"]

Run the tests and this one will fail with too many items in the list. We can fix that by removing the item and then adding it again.

type RecentlyUsedList(capacity:int) =
    let items = ResizeArray<string>(capacity)
    let add item =
        item |> items.Remove |> ignore
        items.Add item
    
    member _.Add(item:string) = add item
    member _.Items = items |> Seq.toList |> List.rev

The ignore is there to remove a compiler warning as items.Remove returns a boolean which we are not interested in.

Run the tests and they should all pass.

Test 5 - Given a list at capacity, add an item not in the list

As our tests pass, we can add another test. We have now reached the tests when the list is at capacity. Add the new module and test:

module ``Given a recently used list that is at capacity`` =
    
    [<Fact>]
    let ``when you add an item not in the list then the new item is added and the oldest item is dropped from the list`` () =
        let sut = RecentlyUsedList(capacity = 3)
        sut.Add("Blue")
        sut.Add("Green")
        sut.Add("Red")

        sut.Add("Yellow")

        sut.Items |> should equal ["Yellow";"Red";"Green"]

Run the tests and this one will fail as it will contain too may items since capacity is only a starting point for the ResizeArray.

Let’s fix that by removing the first item from the list if the list is at capacity:

type RecentlyUsedList(capacity:int) =
    let items = ResizeArray<string>(capacity)
    let add item =
        items.Remove item |> ignore
        if items.Count = items.Capacity then items.RemoveAt 0
        items.Add item
    member _.Add(item:string) = add item
    member _.Items = items |> Seq.toList |> List.rev

Run the tests and they should all pass.

Test 6 - Given a list at capacity, add an item already in the list

Let’s write our test:

    [<Fact>]
    let ``when you add an item already in the list then the item is moved to the start of the list`` () =
        let sut = RecentlyUsedList(capacity = 3)
        sut.Add("Blue")
        sut.Add("Green")
        sut.Add("Red")

        sut.Add("Green")

        sut.Items |> should equal ["Green";"Red";"Blue"]

Run the tests and they all pass. If you think about it, we are duplicating Test 4 as we are moving an existing item to the top of the list. Normally, we would delete this test but I’m going to keep it in for completeness.

As our tests pass, we can do a little refactoring to the last two tests to handle setting up the at capacity list.

module ``Given a recently used list that is at capacity`` =
    
    let createAtCapacityList() =
        let sut = RecentlyUsedList(capacity = 3)
        sut.Add("Blue")
        sut.Add("Green")
        sut.Add("Red")
        sut
    
    [<Fact>]
    let ``when you add an item not in the list then the new item is added and the oldest item is dropped from the list`` () =
        let sut = createAtCapacityList()

        sut.Add("Yellow")

        sut.Items |> should equal ["Yellow";"Red";"Green"]
    
    [<Fact>]
    let ``when you add an item already in the list then the item is moved to the start of the list`` () =
        let sut = createAtCapacityList()
        
        sut.Add("Green")

        sut.Items |> should equal ["Green";"Red";"Blue"]

Run our tests and they should all pass.

We have completed our task and this is what we have finally ended up with:

module RecentlyUsedListTests

open Xunit
open FsUnit.Xunit

// - A new list is empty
// - Given an empty list
//   - Add an item
// - Given a non-empty list, not at capacity
//   - Add an item not in the list
//   - Add an item already in the list
// - Given a list at capacity
//   - Add an item not in the list
//   - Add an item already in the list

type RecentlyUsedList(capacity:int) =
    let items = ResizeArray<string>(capacity)

    let add item =
        item |> items.Remove |> ignore
        if items.Count = items.Capacity then items.RemoveAt 0
        items.Add item
    
    member _.Add(item) = add item
    member _.Items = items |> Seq.toList |> List.rev

module ``Given a newly created recently used list`` =

    [<Fact>]
    let ``then it should contain no items`` () =
        let sut = RecentlyUsedList(capacity = 3)
        
        sut.Items |> should be Empty
        
module ``Given an empty recently used list`` =

    [<Fact>]
    let ``when you add one item then the list contains only that item`` () =
        let sut = RecentlyUsedList(capacity = 3)
        
        sut.Add("Blue")
        
        sut.Items |> should equal ["Blue"]

module ``Given a non-empty recently used list that is not at capacity`` =

    [<Fact>]
    let ``when you add an item not already in list then the item is added to the start of the list`` () =
        let sut = RecentlyUsedList(capacity = 3)
        sut.Add("Blue")

        sut.Add("Green")
        
        sut.Items |> should equal ["Green";"Blue"]

    [<Fact>]
    let ``when you add an item already in list then the item is moved to the start of the list`` () =
        let sut = RecentlyUsedList(capacity = 3)
        sut.Add("Blue")
        sut.Add("Green")
        
        sut.Add("Blue")
        
        sut.Items |> should equal ["Blue";"Green"]

module ``Given a recently used list that is at capacity`` =
    
    let createAtCapacityList() =
        let sut = RecentlyUsedList(capacity = 3)
        sut.Add("Blue")
        sut.Add("Green")
        sut.Add("Red")
        sut
    
    [<Fact>]
    let ``when you add an item not in the list then the new item is added and the oldest item is dropped from the list`` () =
        let sut = createAtCapacityList()

        sut.Add("Yellow")

        sut.Items |> should equal ["Yellow";"Red";"Green"]
    
    [<Fact>]
    let ``when you add an item already in the list then the item is moved to the start of the list`` () =
        let sut = createAtCapacityList()
        
        sut.Add("Green")

        sut.Items |> should equal ["Green";"Red";"Blue"]

It might seem like a lot of work but we have some code that we know is easy to use and that has behaviour that is thoroughly tested.

Red Green Refactor

A simple phrase to help you remember the steps in TDD is Red-Green-Refactor. If your tests are passing, you can write a new failing test (Red). You write enough code to get all of the tests to pass (Green). You can then improve your code in small steps (Refactor). Your tests should still pass. If your tests do not pass, undo your changes back to the last time that they passed and try again.

Never refactor when the tests are failing. Undo your changes and try again.

Summary

I hope that you found this post both useful and informative. TDD is not too difficult if you think about the behaviour you want to test beforehand and follow a few simple rules. The benefits that it provides far outway any potential initial time costs.

This post was more about the journey than the destination. Getting good at TDD is a result of experience and practicing good habits. Your code can only improve by wisely using more of it.