5

How do I make my F# application testable? The application is written mostly using F# functions, and records.

I am aware of How to test functions in f# with external dependencies and I'm aware of the various blog posts that show how easy this is done when your interface only has one method.

Functions are grouped in modules similar to how I would group method in C# classes.

My problems is how do I replace certain "abstractions" when running tests. I need to do this since these abstractions read/write to the DB, talk to services over the network etc. An example of such abstractions is the below repository for storing and fetching people and companies (and their rating).

How do I replace this code in testing? The function calls are hard coded, similar to static method calls in C#.

I have a few posibilities in mind, but not sure if my thinking is too colored of my C# background.

  1. I can implement my modules as interfaces and classes. While this is still F# I feel this is a wrong approach, since I then loose a lot of benefits. This is also argued for in http://fsharpforfunandprofit.com/posts/overview-of-types-in-fsharp/

  2. The code that calls eg. our PersonRepo could take as argument function pointers to all the functions of the PersonRepo. This however, quickly accumulate to 20 or more pointers. Hard for anyone to overview. It also makes the code base fragile, as for every new function in say our PersonRepo I need to add function pointers "all the way up" to the root component.

  3. I can create a record holding all the functions of my PersonRepo (and one for each abstraction I need to mock out). But I'm unsure if I then should create an explicit type e.g. for the record used in lookupPerson the (Id;Status;Timestamp).

  4. Is there any other way? I prefer to keep the application functional.

an example module with side-effects I need to mock out during testing:

namespace PeanutCorp.Repositories
module PersonRepo =
    let findPerson ssn =
        use db = DbSchema.GetDataContext(ConnectionString)
        query {
            for ratingId in db.Rating do
            where (Identifier.Identifier = ssn)
            select (Some { Id = Identifier.Id; Status = Local; Timestamp = Identifier.LastChecked; })
            headOrDefault
        }

    let savePerson id ssn timestamp status rating =
        use db = DbSchema.GetDataContext(ConnectionString)
        let entry = new DbSchema.Rating(Id = id,
                                       Id = ClientId.Value,
                                       Identifier = id,
                                       LastChecked = timestamp,
                                       Status = status,
                                       Rating = rating
        )
        db.Person.InsertOnSubmit(entry)
        ...

    let findCompany companyId = ...

    let saveCompany id companyId timestamp status rating = ...

    let findCachedPerson lookup identifier = ...
Community
  • 1
  • 1
Carlo V. Dango
  • 13,322
  • 16
  • 71
  • 114
  • It looks like several of the functions in your PersonRepo module might be little more than query expressions with some extra side effects attached for connecting to the database. A good strategy might be to abstract away all the database connectivity, instead taking an argument to something than can be acted upon by the query expression. Then, during testing, you can then insert a simpler mock database object and test that the correct results are extracted from it. Would your other functions suit that approach? – TheInnerLight Nov 02 '15 at 22:59
  • Sure but how are we to do this? – Carlo V. Dango Nov 03 '15 at 09:40

2 Answers2

9

This however, quickly accumulate to 20 or more pointers.

If that's true, then that's the number of dependencies those clients already have. Inverting the control (yes: IoC) would only make that explicit instead of implicit.

Hard for anyone to overview.

In light of the above, hasn't that already happened?

Is there any other way? I prefer to keep the application functional.

Your can't 'keep' the application functional, because it isn't. The PersonRepo module contains functions that are not referentially transparent. Any other function that depends on such a function is also automatically not referentially transparent.

If most of the application transitively depends on such PersonRepo functions, it means that little (if any) of it is referentially transparent. That means it isn't Functional. It's also difficult to unit test, for exactly that reason. (The converse is also true: Functional design is intrinsically testable,)

Ultimately, Functional design also needs to deal with functions that can't be referentially transparent. The idiomatic approach is to push those functions to the edges of the system, so that the core of the function is pure. That's actually quite similar to Hexagonal Architecture, but in e.g. Haskell, it's formalized through the IO Monad. Most good Haskell code is pure, but at the edges, functions work in the context of IO.

In order to make a code base testable, you'll need to invert control, just as IoC is used for testing in OOP.

F# gives you an excellent tool for that, because its compiler enforces that you can't use anything until you've defined it. Thus, the 'only thing' you need to do is to put all the impure functions last. That ensures that all the core functions can't use the impure functions, because they aren't defined at that point.

The tricky part is to figure out how to use functions that aren't defined yet, but my preferred way in F# is to pass functions as arguments.

Instead of using PersonRepo.savePerson from another function, that function ought to take a function argument that has the signature that the client function needs:

let myClientFunction savePerson foo bar baz =
    // Do something interesting first...
    savePerson (Guid.NewGuid ()) foo DateTimeOffset.Now bar baz
    // Then perhaps something else here...

Then, when you compose your application, you can compose myClientFunction with PersonRepo.savePerson:

let myClientFunction = myClientFunction PersonRepo.savePerson

When you want to unit test myClientFunction, you can supply a Test Double implementation of savePerson. You don't even have to use dynamic mocks, because the only requirement is that savePerson has the correct type.

Mark Seemann
  • 225,310
  • 48
  • 427
  • 736
  • What do you mean I have 20+ dependencies? Had this been C# you would say I had 4. I have no problem juggling with a PersonRepo, A CompanyRating, And a couple of RestFacade's. Why are you against trying to group together the functions into a record or something? By claiming that each function is a dependency on its own, aren't you then ignoring the `module` metchanism of the language that is in fact grouping functions together? – Carlo V. Dango Nov 02 '15 at 07:33
  • 2
    @CarloV.Dango It's the [ISP](https://en.wikipedia.org/wiki/Interface_segregation_principle) applied. When you bundle functions together, you can't subsequently separate them. The more you group things together, the more you violate the ISP. This is an object-oriented principle that I've found also applies in Functional design, but I design my object-oriented code the same way: one method per interface. So in C#, I would also have said that you have 20+ dependencies if you follow [SOLID](http://bit.ly/YOB2I0). – Mark Seemann Nov 02 '15 at 08:11
  • 2
    @MarkSeemann I completely disagree with the suggestion that ISP implies one method per interface. The article you linked says: "(ISP) states that no client should be forced to depend on methods it does not use". I'm sure there are many circumstances when a one method interface is appropriate but there are equally times when it is not. It is better to think of a properly segregated interface as defining a discrete set of behaviours which are highly cohesive. Unnecessarily segregated interfaces make it complex, verbose and unintuitive to define functions which act on logical, discrete units. – TheInnerLight Nov 02 '15 at 21:18
  • @MarkSeemann how would you assert that the savePerson TestDouble was actually called in your unit test? –  Feb 28 '16 at 20:22
  • 1
    @parajao The easiest way I've found is to use a boolean reference cell and set it to `true` within the Mock function. – Mark Seemann Feb 28 '16 at 20:45
0

I recommend that you remove all the DbSchema.GetDataContext(ConnectionString) resource lookups and instead receive db as an argument (or a function to create the db if you want to enclose it in a using block).

Example:

let findPerson dbCreator ssn =
    using (dbCreator()) (fun db ->
        query {
            for ratingId in db.Rating do
            where (ratingId.Identifier = ssn)
            select (Some { Id = ratingId .Id; Status = Local; Timestamp = ratingId.LastChecked; })
            headOrDefault
        })

where dbCreator has type unit -> 'a, with 'a being of the type of db in your original example.

The findPerson no longer depends upon a specific resource, it simply receives an argument to something it can act upon. It's now trivial to replace 'db' with some other object which is more suitable for testing with.

This should not be so different to a dependency injection style approach you might use in C#. In that case, the above module might instead be a class with a constructor taking an argument containing a database resource to depend upon.

The savePerson function is a little more complex because you have an additional side effect to write into the database. You could keep this behaviour inside the savePerson function or pass it in as an additional argument. The key thing to remember as you're coming from an OO background, is that functional code will let you pass in far more complex behaviours as arguments.

TheInnerLight
  • 12,034
  • 1
  • 29
  • 52
  • 1
    Effectively you are suggesting the same as Seeman aren't you? Pull out all calls to functions having side effects and take them as arguments to the top-level method. My fear is the generality of this approach as you quickly end up with many dependencies if you have a business flow that actually does something (with the real world) and not just calculates the simplest cases of score calculations of tennis games – Carlo V. Dango Nov 05 '15 at 20:12
  • @CarloV.Dango I think I agree with Seeman on much of the theory but from the perspective of code organisation, I don't see a problem with passing in a number of side-effect causing functions as one interface provided those behaviours are cohesive. You don't need to make every dependency separate and explicit unless you need the capability to vary them independently of one another. In many cases it could be enough to have one production behaviour and one test behaviour. – TheInnerLight Nov 05 '15 at 20:31