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.
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/
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.
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)
.
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 = ...
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.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With