dimanche 1 novembre 2015

How to mock out rich dependencies when testing F#

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://ift.tt/1M5fxUJ

  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 = ...

Aucun commentaire:

Enregistrer un commentaire