When coaching some of our F# customers, one of the common questions we hear is "how do we make our code testable?". I would like to talk briefly about this from a code-focused perspective and give some different ways of refactoring a relatively simple code snippet to aid testability, and some of the pros and cons of each of them.
The following examples demonstrate some different approaches to testable F# code, and how you might look at refactoring your logic to separate out dependencies and enable better testability - and what the pros and cons of these approaches may be.
1: 2: 3: 4: 5: 6: 7: 8: 9: 10: 11: 12: 13: 14: 15: 16: 17: 18: 19: 20:
This is some standard F#, with a simple set of functions in a standard call chain. The
orchestrator function handles the, well, orchestration of data access and logic for some
unknown business purpose. The code calls directly into the
DB module in order to load and save
data, intermingled with validation.
On the one hand this code is simple to follow. There is no use of higher-order functions, no
dependency injection etc.. Of course, the flip side is that functions such as
trySavePerson are untestable in the sense that they go straight to a real database.
Now, a simplistic way to improve matters (and often this will suffice) is to simply start to use
higher order functions - in other words, supply functions as arguments to other functions as a
way to compose behaviours together. In our case, this means replacing the calls to the
with fake functions that have the same structural signature.
1: 2: 3: 4: 5: 6: 7: 8: 9: 10: 11: 12: 13:
This simple refactoring has improved our code in the sense that
trySavePerson is now testable
insofar as we can replace the call to
savePerson with a faked version e.g.
1: 2: 3:
Of course, this adds some complexity to our code - two higher order functions (HOFs). HOFs are extremely useful as a way to compose behaviours together or for simple mocking, but in larger systems they can become unweildy if used without care:
Firstly, it's easy to create "unusual" higher order functions that represent bizarre abstractions with difficult to understand type signatures (especially if generics or secondary HOFs interact with it).
Since we don't tend to use dependency injection in the conventional OO sense, we have to manually
supply all HOFs to functions. Secondly, you need to pass the HOFs all the way down the call chain
from the "top level" which decides which implementation of the HOF to use. In this case, witness
DB.savePerson is supplied at the top level and is "threaded" down through
trySavePerson. Now, this simple example only contains a call stack two functions deep, but imagine
if it were a larger application with potentially dozens of callsites: it can quickly become
difficult to maintain.
Also, consider how
orchestrator requires two dependencies to be passed in. For larger functions
(or function "roots"), this number of HOFs can quickly grow and become difficult to manage. One
alternative to this is to create an interface which contains all (or some) dependencies bundled
together, and throw this single object around throughout your system. Perhaps you could also use
F#4.6's new anonymous records feature
to assist here!
Another alternative is to use a "bootstrapper" function which is responsible for "wire up" of dependencies.
1: 2: 3: 4: 5: 6: 7: 8: 9: 10: 11: 12: 13: 14:
What's important to see in this example is that we ensure that HOFs are never passed more than one
level deep. We achieve this by performing the wireup of the lowest-level functions first, and
then working back up the call chain before arriving at the top with a fully "injected" function.
In our case, that's the
trySavePersonToDb function, which we then supply into the
There's now no need to explicitly supply dependencies all the way down the chain within your
business logic, because the bootstrapper does all this for you.
I sometimes call this an "inside out" refactoring, because you essentially invert the way in which
dependencies are managed - instead of an explcit call chain of
(A dep) which internally calls
you end up with a call chain such as
A (B dep) in which
dep is provided as an argument to
and the resulting function is itself provided as an argument to
A no longer has any knowledge
On the one hand, this means that functions such as
orchestrator are more easily testable and
simpler to reason about. On the other hand, you now have a growing bootstrapper function to
maintain which will be responsible for handling the wireup throughout your application. Depending
on how you pass dependencies around, this may be entirely managable, or not.
All the examples above don't try to alter the fundamental nature of the functions themselves. An alternative is to look at changing the responsibilities of each function directly and seeing if that lends itself to a better outcome, with easier composition.
1: 2: 3: 4: 5: 6: 7: 8: 9: 10: 11: 12: 13: 14:
In this version, we've split out the validation logic from the save-to-database logic completely.
Doing this immediately makes our validation code inherently testable, and easier to reason about:
Instead of calling the database, we simply return
Ok() for the happy path.
Now, the composition of "validation" and "saving to the database" are performed directly in the
bootstrapper using the
>> operator to compose both functions; this is typical "railway-oriented"
programming. We can no longer "test" the orchestrated function that combines validation and save,
since the composed function only exists in the bootstrapper, and is directly coupled to the physical
database again. However, assuming we have trust in
Result.bind and the
>> operator, and can
ensure that the
validate function behaves correctly, this may be completely reasonable.
Using the above approach, in this case it's possible to completely do away with the orchestrator
function using the
>> (composition) operator. Of course, in a more fully-feature application, you
may have another function (or functions!) between loading and validation that performs some business
1: 2: 3: 4: 5: 6: 7: 8: 9: 10:
There are some excellent online resources on the "Ports and Adapters" architecture by people such as Mark Seeman that are well worth checking out. One of the goals of this architecture is to push your impure functions to the boundaries of the application, leaving a pure center. In F#, since it's not easily possible to guarantee pure functions, one way of doing this is to simply program against values, and not higher order functions at all.
This is essentially our "compose to the max" example above, but is subtly different from a
philosphical standup if nothing else. Here, we view our "pure" application as the
function (along with any other "pure" functions that work soley on data), whilst the load and save
are "ports" to the outside world; we compose the "impure" and "pure" functions together in our
The github repo also illustrates how your testing might be affected by these different ways of composing your code together.
This is by no means a complete, in-depth analysis of how to refactor dependencies away in F# code, nor am I stating which of the alternatives above is "better" or "worse". Indeed, there are also other ways of solving this problem. For example, I've seen systems that had a global "static" dependencies value which was set once at the start of the application as another way. It's certainly not a "purist" FP solution, but in this case it worked for that team.
The most important thing is to be aware of some of the trade offs for the alternatives, and to try them out yourself. Take one hour out of your week, create a new branch in git and try to refactor a subset of your codebase in a different style. What are the costs and benefits? Were there any unforseen issues? What impact did it have on testability?
Hopefully you enjoyed this - and have
fun _ -> Ok()!