While F# is a functional-first language, it's okay to occasionally be pragmatic and use mutable state, for example to easily write some sustainable state-based tests.

This article is an instalment in an article series about how to move from interaction-based testing to state-based testing. In the previous article, you saw how to write state-based tests in Haskell. In this article, you'll see how to apply what you've learned in F#.

The code shown in this article is available on GitHub.

A function to connect two users #

This article, like the others in this series, implements an operation to connect two users. I explain the example in details in my two Clean Coders videos, Church Visitor and Preserved in translation.

Like in the previous Haskell example, in this article we'll start with the implementation, and then see how to unit test it.

// ('a -> Result<User,UserLookupError>) -> (User -> unit) -> 'a -> 'a -> HttpResponse<User>
let post lookupUser updateUser userId otherUserId =
    let userRes =
        lookupUser userId |> Result.mapError (function
            | InvalidId -> "Invalid user ID."
            | NotFound  -> "User not found.")
    let otherUserRes =
        lookupUser otherUserId |> Result.mapError (function
            | InvalidId -> "Invalid ID for other user."
            | NotFound  -> "Other user not found.")
 
    let connect = result {
        let! user = userRes
        let! otherUser = otherUserRes
        addConnection user otherUser |> updateUser
        return otherUser }
 
    match connect with Ok u -> OK u | Error msg -> BadRequest msg

While the original C# example used Constructor Injection, the above post function uses partial application for Dependency Injection. The two function arguments lookupUser and updateUser represent interactions with a database. Since functions are polymorphic, however, it's possible to replace them with Test Doubles.

A Fake database #

Like in the Haskell example, you can implement a Fake database in F#. It's also possible to implement the State monad in F#, but there's less need for it. F# is a functional-first language, but you can also write mutable code if need be. You could, then, choose to be pragmatic and base your Fake database on mutable state.

type FakeDB () =
    let users = Dictionary<int, User> ()
 
    member val IsDirty = false with get, set
 
    member this.AddUser user =
        this.IsDirty <- true
        users.Add (user.UserId, user)
 
    member this.TryFind i =
        match users.TryGetValue i with
        | false, _ -> None
        | true,  u -> Some u
 
    member this.LookupUser s =
        match Int32.TryParse s with
        | false, _ -> Error InvalidId
        | true, i ->
            match users.TryGetValue i with
            | false, _ -> Error NotFound
            | true, u -> Ok u
 
    member this.UpdateUser u =
        this.IsDirty <- true
        users.[u.UserId] <- u

This FakeDB type is a class that wraps a mutable dictionary. While it 'implements' LookupUser and UpdateUser, it also exposes what xUnit Test Patterns calls a Retrieval Interface: an API that tests can use to examine the state of the object.

Immutable values normally have structural equality. This means that two values are considered equal if they contain the same constituent values, and have the same structure. Mutable objects, on the other hand, typically have reference equality. This makes it harder to compare two objects, which is, however, what almost all unit testing is about. You compare expected state with actual state.

In the previous article, the Fake database was simply an immutable dictionary. This meant that tests could easily compare expected and actual values, since they were immutable. When you use a mutable object, like the above dictionary, this is harder. Instead, what I chose to do here was to introduce an IsDirty flag. This enables easy verification of whether or not the database changed.

Happy path test case #

This is all you need in terms of Test Doubles. You now have test-specific LookupUser and UpdateUser methods that you can pass to the post function.

Like in the previous article, you can start by exercising the happy path where a user successfully connects with another user:

[<Fact>]
let ``Users successfully connect`` () = Property.check <| property {
    let! user = Gen.user
    let! otherUser = Gen.withOtherId user
    let db = FakeDB ()
    db.AddUser user
    db.AddUser otherUser
 
    let actual = post db.LookupUser db.UpdateUser (string user.UserId) (string otherUser.UserId)
 
    test <@ db.TryFind user.UserId
            |> Option.exists
                (fun u -> u.ConnectedUsers |> List.contains otherUser) @>
    test <@ isOK actual @> }

All tests in this article use xUnit.net 2.3.1, Unquote 4.0.0, and Hedgehog 0.7.0.0.

This test first adds two valid users to the Fake database db. It then calls the post function, passing the db.LookupUser and db.UpdateUser methods as arguments. Finally, it verifies that the 'first' user's ConnectedUsers now contains the otherUser. It also verifies that actual represents a 200 OK HTTP response.

Missing user test case #

While there's one happy-path test case, there's four other test cases left. One of these is when the first user doesn't exist:

[<Fact>]
let ``Users don't connect when user doesn't exist`` () = Property.check <| property {
    let! i = Range.linear 1 1_000_000 |> Gen.int
    let! otherUser = Gen.user
    let db = FakeDB ()
    db.AddUser otherUser
    db.IsDirty <- false
    let uniqueUserId = string (otherUser.UserId + i)
 
    let actual = post db.LookupUser db.UpdateUser uniqueUserId (string otherUser.UserId)
 
    test <@ not db.IsDirty @>
    test <@ isBadRequest actual @> }

This test adds one valid user to the Fake database. Once it's done with configuring the database, it sets IsDirty to false. The AddUser method sets IsDirty to true, so it's important to reset the flag before the act phase of the test. You could consider this a bit of a hack, but I think it makes the intent of the test clear. This is, however, a position I'm ready to reassess should the tests evolve to make this design awkward.

As explained in the previous article, this test case requires an ID of a user that doesn't exist. Since this is a property-based test, there's a risk that Hedgehog might generate a number i equal to otherUser.UserId. One way to get around that problem is to add the two numbers together. Since i is generated from the range 1 - 1,000,000, uniqueUserId is guaranteed to be different from otherUser.UserId.

The test verifies that the state of the database didn't change (that IsDirty is still false), and that actual represents a 400 Bad Request HTTP response.

Remaining test cases #

You can write the remaining three test cases in the same vein:

[<Fact>]
let ``Users don't connect when other user doesn't exist`` () = Property.check <| property {
    let! i = Range.linear 1 1_000_000 |> Gen.int
    let! user = Gen.user
    let db = FakeDB ()
    db.AddUser user
    db.IsDirty <- false
    let uniqueOtherUserId = string (user.UserId + i)
 
    let actual = post db.LookupUser db.UpdateUser (string user.UserId) uniqueOtherUserId 
 
    test <@ not db.IsDirty @>
    test <@ isBadRequest actual @> }
 
[<Fact>]
let ``Users don't connect when user Id is invalid`` () = Property.check <| property {
    let! s = Gen.alphaNum |> Gen.string (Range.linear 0 100) |> Gen.filter isIdInvalid
    let! otherUser = Gen.user
    let db = FakeDB ()
    db.AddUser otherUser
    db.IsDirty <- false
 
    let actual = post db.LookupUser db.UpdateUser s (string otherUser.UserId)
 
    test <@ not db.IsDirty @>
    test <@ isBadRequest actual @> }
 
[<Fact>]
let ``Users don't connect when other user Id is invalid`` () = Property.check <| property {
    let! s = Gen.alphaNum |> Gen.string (Range.linear 0 100) |> Gen.filter isIdInvalid
    let! user = Gen.user
    let db = FakeDB ()
    db.AddUser user
    db.IsDirty <- false
 
    let actual = post db.LookupUser db.UpdateUser (string user.UserId) s
 
    test <@ not db.IsDirty @>
    test <@ isBadRequest actual @> }

All tests inspect the state of the Fake database after the calling the post function. The exact interactions between post and db aren't specified. Instead, these tests rely on setting up the initial state, exercising the System Under Test, and verifying the final state. These are all state-based tests that avoid over-specifying the interactions.

Summary #

While the previous Haskell example demonstrated that it's possible to write state-based unit tests in a functional style, when using F#, it sometimes make sense to leverage the object-oriented features already available in the .NET framework, such as mutable dictionaries. It would have been possible to write purely functional state-based tests in F# as well, by porting the Haskell examples, but here, I wanted to demonstrate that this isn't required.

I tend to be of the opinion that it's only possible to be pragmatic if you know how to be dogmatic, but now that we know how to write state-based tests in a strictly functional style, I think it's fine to be pragmatic and use a bit of mutable state in F#. The benefit of this is that it now seems clear how to apply what we've learned to the original C# example.

Next: An example of state-based testing in C#.



Wish to comment?

You can add a comment to this post by sending me a pull request. Alternatively, you can discuss this post on Twitter or somewhere else with a permalink. Ping me with the link, and I may respond.

Published

Monday, 25 March 2019 06:34:00 UTC

Tags



"Our team wholeheartedly endorses Mark. His expert service provides tremendous value."
Hire me!
Published: Monday, 25 March 2019 06:34:00 UTC