When to Unit Test in F#


abraham_getprog-fsharp_hiresmeap

Note: This article has been excerpted from my upcoming book, Get Programming with F#. It provides an overview of different “levels” of unit testing, and how and where they’re appropriate in F#. We’ll also discuss different forms of unit testing practices, including test driven development (TDD), and finally see how to write simple unit tests in F# using a popular test framework.

Save 37% off Get Programming with F# with code fccabraham at manning.com

Unit Testing complexity

Let’s start by stating plain and simple that, yes, there’s still a place for unit testing in F#. Although its type system allows us to implement business rules in code to prevent illegal states from being representable (and this is a worthy goal), there are many rules that aren’t easy to encode within F#’s type system. Let’s partition tests into a few groups, and see in which languages you might more commonly write these sorts automated unit tests for them.

Table 1 – Types of Unit Testing

Type of Test Example Typical Languages
Simple Type Is the value of the Age property an integer? Javascript
Complex Type Is the value of the Postcode field a Postcode? Javascript, C#
Simple Rule Only an in-credit customer can withdraw funds. Javascript, C#
Complex Rule Complex rules engine with multiple compound rules. JS, C#, F#

The point is that the stronger the type system, the fewer tests you should need. Consider a language such as Javascript – at compile time there’s no real type checking, and even at runtime you can assign a number to a property meant to store a string, whilst accidentally assigning a value to a misspelled property (Javascript will merrily carry on in such a situation – hence why languages such as Typescript are becoming popular) – which explains why unit testing is important in such a language. In effect, you’re writing a custom compiler for each of your programs! Languages such as C# eliminate the need for such rudimentary tests, yet even in C# anything more than the most simplistic rules can often lead to the need for unit tests to maintain confidence that your application is doing what it’s meant to do. Lastly, we have F#. Many cases exist where I’d suggest that unit testing doesn’t make sense, but you might still want unit tests for complex rules or situations where the type system doesn’t protect you. Here are some examples: –

  • Complex business rules, particularly with conditionals – For more complex rules, or nested rules that are combined to perform some overall feature, you’ll still probably want some form of unit testing.
  • Complex parsing – Parsing code can be tricky, and you might want some form of unit testing to ensure regressions don’t occur.
  • A list that must have a certain number of elements in it – some programming languages (such as Idris) allow you to encode this within the type system. You can specify that a function takes in an argument which is a list of five elements, at compile time! These languages are known as dependently typed languages; F# isn’t such a language.

Conversely, here are several cases in which you can often simplify the need for unit testing because the compiler gives us more confidence that the code is doing the correct thing: –

  • Expressions – Probably the most important thing in F# to help us is our tendency to write code as expressions, with immutable values. This helps prevent many types of bugs that we’d otherwise need to resort to unit testing for: functions that take in a value and return another are simpler to reason about and test than those that require complex setup, like when the state is based on previous method calls.
  • Exhaustive Pattern Matching – The F# compiler will tell us that we’ve missed out cases for conditional logic. This is particularly useful and powerful when pattern matching over tupled values because you can perform truth-table style logic and be confident that you’ve dealt with every case.
  • Single Case Discriminated Unions – These provide us with confidence that we have not accidentally mixed up fields of the same type, e.g. Customer Name and Address Line 1 by providing a “type of type” e.g. Name of string, Address of string etc., which prevents this sort of error.
  • Option types – Not having null in the type system for F# values means we generally don’t need to worry about nulls when working within an F# domain. Instead, we must deal with the possibility of “absence-of-value” when it’s a real possibility.

TDD or Regression Testing?

We’ve discussed some high-level situations where you might write unit tests, but we have not said when to write them, i.e. before writing production code – test-driven development – or after the fact? I can rely on my experience writing production systems in F# and say that, as someone who was a complete TDD zealot in C#, I don’t perform TDD any more. Not because I can’t be bothered or because it’s not possible in F# – I’ve tried it. The language, when combined with the REPL, means that I don’t feel the need for it. My productivity is higher in F# without TDD – and this includes bug fixing – than in C# with TDD.

Instead, I write unit tests for low-level code which are either fiddly and complex, or at a reasonably high level – perhaps something that can be matched to a part of a specification – but generally only after I’ve experimented with the code in the REPL, written the basic functionality, and made sure it works nicely within the rest of the codebase.

Your mileage may vary, and I don’t want to sound dogmatic, but I recommend you at least try writing F# without unit tests, and see how you get on. Alternatively, write the tests first – you’ll most likely find that you didn’t need them (particularly with the REPL). Follow the sorts of rules and practices that we’ve covered and you’ll probably find that for many cases where you might have resorted to unit testing or even TDD, you won’t need to any longer. The first time you perform a compound pattern match and the compiler tells you about a case that you hadn’t thought of – it hits you that, yes, a compiler can replace many unit tests.

The F# equivalent of TDD

Mark Seeman (as far as I’m aware) coined the phrase Type Driven Development, which has become known as a kind of F# version of Test Driven Development. This refers to the idea that you use your types to encode business rules and make illegal states unrepresentable –driving development and rules through the type system, rather than through unit tests.

Quick Check

  1. What’s the relationship between a type system and unit tests?
  2. Name any two features of the F# language that reduce the need for unit testing.

Basic Unit Testing in F#

Okay, enough theory – let’s get on with the practical stuff, and write some unit tests. I’m going to use the popular Xunit test framework here, but you can also happily use NUnit or MSTest as well – they all work in the same way.

NUnit or XUnit?

Both NUnit and XUnit test frameworks are popular, both work seamlessly with F#, and there’s no reason to move from one to another because of F#. You might want to try out the new F#-specific unit testing library called Expecto. It’s different – rather than a test framework with attributes, it’s a flexible runner that can make tests out of any function. It’s beyond the scope of this article to show it, but you should check it out.

Writing our first unit tests

Now You Try

Let’s write a set of unit tests for some arbitrary (simple) code in F# using Xunit.

  1. Create a new solution in Visual Studio and create a single F# class library. Normally you’d probably create a separate test project, but it’s not needed for this example.
  2. Add the XUnit and XUnit.Runner.VisualStudio packages to the project.
  3. Create a new file, BusinessLogic.fs that contains the logic that we’ll test out: –
module BusinessLogic

// A simple domain
type Employee = { Name : string; Age : int }
type Department = { Name : string; Team : Employee list }

// Some simple functions acting on our domain
let isLargeDepartment department = department.Team.Length > 10
let isLessThanTwenty person = person.Age < 20 let isLargeAndYoungTeam department =          department |> isLargeDepartment
    && department.Team |> List.forall isLessThanTwenty
  1. Let’s now write our first team. Start by creating a new file, BusinessLogicTests.fs (remember to ensure it lives underneath BusinessLogic.fs in Solution Explorer).
  2. Enter the following code in the new file: –
module BusinessLogicTests

open BusinessLogic
open Xunit

// A standard XUnit test using the [<Fact>] attribute
// and Assert class
[<Fact>]
let isLargeAndYoungTeam_TeamIsLargeAndYoung_ReturnsTrue() =
    let department =
        { Name = "Super Team"
          Team =
              [ for i in 1 .. 15 ->
                  { Name = sprintf "Person %d" i; Age = 19 } ] }

    Assert.True(department |> isLargeAndYoungTeam)
  1. Rebuild the project. You should see the test show up in Test Explorer in Visual Studio; run it and the test will go green.

Because a module in F# compiles down to a static class in .NET, and let-bound functions in F# compile down to static methods, everything works. You can use all the extra features in XUnit and NUnit without a problem, e.g. theories, parameterized tests – they all work.

Figure 40-1
Figure 1 – F# tests show up in the VS Test Explorer as expected

Removing Class Names from Tests

In Figure 1, you’ll notice that the test name isn’t prefixed with the class name. To achieve this, you need to add an app setting key “xunit.methodDisplay” to the app.config file of the test assembly, and set it’s value to “method”.

Naming Tests in F#

Entire blogs are out there (and probably books) on how to name unit tests. This can be quite an emotive subject in many organisations – not to mention difficult to keep consistent. I’ve seen many different naming “standards”, from conventions such as Given_When_Then (which is a popular one for people following Behaviour Driven Development) to ones such as Method_Scenario_Expected (recommended by Roy Osherove). Nothing should stop you from following those standards (as I’ve done in the listing above) but thanks to F#’s backtick methods, you can eliminate this debate completely and name the method based on exactly what it’s testing: –

let ``Large, young teams are correctly identified``() =

Believe it or not, not only does this work, but it works beautifully. Try it – rename the test by starting and stopping the name with double back-ticks, recompile and then view Test Explorer again – much nicer! But don’t stop there – you can go one step further by renaming the test module name as well, and now the test explorer looks as follows:

Figure 40-2
F# backtick methods can aid readability of unit tests

Much nicer, isn’t it? Aside from this, unit testing in F# acts exactly as you’d expect in C#.

What about BDD?

I touched on BDD earlier. You can use frameworks such as SpecFlow to write BDD tests, and benefit from the extra readability that backtick members give you, no problem. Another option is TickSpec. This is a F# library that works with Cucumber format tests, but it is extremely lightweight and automatically binds up tests to features based on naming convention etc. It’s cool and definitely worth looking at.

If you want to learn more about F#, go download the free first chapter of Get Programming with F# and see this Slideshare presentation for more information and a discount code.

Leave a comment