In modern .Net Solutions, projects using different languages can happily co-exist. This means that you can use the best tool for the job such as F# for your domain logic and C# for your UI or as we do in this blog post, test C# code with F#.

This will be the first post of many on .Net Interop. Anything that increases the use of F# has to be a good thing.

If you’re interested in learning F#, download my free 200-page ebook Essential F#. It’s designed to get C# devs up to speed in F# very quickly.

Now, back to the blog post. Let’s get started!

Creating the Solution

  1. Create a new Library project, set the language to C#, and call it CSharpLibrary. Name the solution DotNetInterop.
  2. Create a new Test project, choose Xunit and F#, and call it FSharpTesting.
  3. Add a reference to CSharpLibrary project to the FSharpTesting project.
  4. Add the FsUnit.Xunit Nuget package to the test project.
  5. Build the solution.

Writing some C#

Rename the created file RecentlyUsedList.cs and add the following code to it:

using System.Collections.Immutable;

namespace CSharpLibrary;

public class RecentlyUsedList
{
    private readonly List<string> _items;

    public RecentlyUsedList(int capacity)
    {
        _items = new List<string>(capacity);
    }

    public void Add(string item)
    {
        _items.Remove(item);
        if (_items.Count == _items.Capacity) { _items.RemoveAt(0); }
        _items.Add(item);
    }

    public ImmutableList<string> Items => 
        _items.ToImmutableList().Reverse();
}

First F# Test

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

module Tests

open CSharpLibrary
open Xunit
open FsUnit.Xunit

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

    [<Fact>]
    let ``then the list contains no items`` () =
        let sut = RecentlyUsedList(3)
        
        sut.Items |> should be Empty

Notice that we don’t need to new up an instance of a class in F#. We do use new for classes that implement the IDisposible interface.

The biggest advantage of using F# for tests is the way that we can use modules for grouping tests and the use of the double backtick `` allows us to use proper sentences rather than odd casing or using seperators.

Run the test. It should pass. If not, rebuild the solution and try again.

The Next Test

Add the following below the existing test code:

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

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

Most C# collections are seen as a Sequence by F#. Sequence is equivalent to IEnumerable<T> in C#. Comparing Sequences can be done but when they are quite small, like the ones in our tests, we will convert them to F# Lists, where we can use structural equality.

Run the tests and they should pass.

The Rest of the Tests

The remainder of the tests follow the same pattern as the last test. Add this code under the existing tests.

module ``Given a non-empty and non-capacity recently used list`` =

    [<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(3)
        sut.Add("Item1")

        sut.Add("Item2")
        
        let expected = ["Item2";"Item1"]
        sut.Items |> Seq.toList |> should equal expected

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

module ``Given a recently used list that is at capacity`` =
    
    let atCapacityList () =
        let rul = RecentlyUsedList(3)
        rul.Add("Item1")
        rul.Add("Item2")
        rul.Add("Item3")
        rul
    
    [<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 = atCapacityList()

        sut.Add("Item4")

        let expected = ["Item4";"Item3";"Item2"]
        sut.Items |> Seq.toList |> should equal expected
    
    [<Fact>]
    let ``when you add an item already in the list then the item is moved to the start of the list`` () =
        let sut = atCapacityList()

        sut.Add("Item2")

        let expected = ["Item2";"Item3";"Item1"]
        sut.Items |> Seq.toList |> should equal expected

Run the tests and they should all pass.

Summary

Using F# for your tests in .Net is a great way to improve the readability of your tests. You don’t need to know much about F# to use it for some or all of your tests.

In future posts, I’ll endeavour to show much more interop, so that you can make an informed choice about which tools to use for each job.