Asik
Asik

Reputation: 22133

How to make this function testable?

I have a function responsible for collecting a bunch of configurations and making a bigger configuration out of all these parts. So it's basically:

let applyUpdate updateData currentState =
    if not (someConditionAbout updateData) then
        log (SomeError)

    let this = getThis updateData currentState.Thingy
    let that = getThat updateData currentState.Thingy
    let andThat = createThatThing that this updateData

    // blablablablablabla

    { currentState with
        This = this
        That = that
        AndThat = andThat
        // etc. }

I currently have unit tests for getThis, getThat, createThatThing, but not for applyUpdate. I don't want to re-test what getThis and etc. are doing, I just want to test the logic specific to applyUpdate and just stub getThis. In an object-oriented style, these would be passed via an interface through dependency injection. In a functional style I'm unsure as to how to proceed:

// This is the function called by tests
let applyUpdateTestable getThisFn getThatFn createThatThingfn etc updateData currentState =
    if not (someConditionAbout updateData) then
        log (SomeError)

    let this = getThisFn updateData currentState.Thingy
    // etc

    { currentState with
        This = this
        // etc. }

// This is the function that is actually called by client code
let applyUpdate = applyUpdateTestable getThis getThat etc

This seems the functional equivalent of Bastard Injection, but beyond that I'm mainly concerned with:

How do deal with these problems in functional programming?

Upvotes: 8

Views: 640

Answers (2)

Mark Seemann
Mark Seemann

Reputation: 233150

The answer by Scott (@Grundoon) covers the more direct translation from OOP to FP. It's appropriate if you expect one of the getThis, getThat functions to be impure.

In general, passing functions as arguments to other functions is quite a functional thing to do (the receiving function is called a higher-order function, then), but it should be done in the interest of enabling variability. Adding extra function arguments only for testing purposes leads to what David Heinemeier Hansson calls test-induced damage.

In this answer, I'd like to offer another perspective, although I wish to stress that Scott's answer aligns with my own thinking (and that I've upvoted it). It fits F#, because F# is a hybrid language, and implicitly impure functions are possible.

In a strictly functional language (like Haskell), however, functions are pure by default. If we assume that getThis, getThat, etcetera are all referentially transparent (pure), function calls can be replaced with their return values.

This means that you don't have to replace them with Test Doubles.

Instead, you can simply write your tests like this:

[<Fact>]
let testExample () =
    // Create updateData and currentState values here...

    let actual = applyUpdate updateData currentState

    let expected = 
        { currentState with
            This = getThis updateData currentState.Thingy
            That = getThat updateData currentState.Thingy
            // etc. }
    expected =! actual // assert that expected equals actual

You could argue that this test only duplicates the production code, but so would a test using OO-style Test Doubles. I assume that the real problem is more complex than the OP, which doesn't really seem to warrant a test of the applyUpdate function.

You could also argue that this test isn't a unit test, and I'd agree on the semantics; I call such tests Facade Tests.

Pure functions are intrinsically testable, so there's no reason to change their design to make them 'testable'.

Upvotes: 3

Grundoon
Grundoon

Reputation: 2764

You said:

In an object-oriented style, these would be passed via an interface through dependency injection.

And the same approach is used in FP, but rather than injecting via the object constructor, you "inject" as parameters to the function.

So you are on the right track with your applyUpdateTestable, except that this would be also used as real code, not just as testable code.

For example, here's the function with the three extra dependencies passed in:

module Core =   
    let applyUpdate getThisFn getThatFn createThatThingfn updateData currentState =
        if not (someConditionAbout updateData) then
            log (SomeError)

        let this = getThisFn updateData currentState.Thingy
        // etc

        { currentState with
            This = this
            // etc. }

Then, in the "production" code, you inject the real dependencies:

module Production =         
    let applyUpdate updateData currentState = 
        Core.applyUpdate Real.getThis Real.getThat Real.createThatThingfn updateData currentState

or more simply, using partial application:

module Production =         
    let applyUpdate = 
        Core.applyUpdate Real.getThis Real.getThat Real.createThatThing

and in the test version, you inject the mocks or stubs instead:

module Test =       
    let applyUpdate = 
        Core.applyUpdate Mock.getThis Mock.getThat Mock.createThatThing

In the "production" example above, I statically hard-coded the dependencies on the Real functions, but alternatively, just as with OO style dependency injection, the production applyUpdate could be created by some top-level coordinator and then passed into the functions that need it.

This answers your questions, I hope:

  • The same core code is used for both production and testing
  • If you statically hard-code the dependencies, you can still use F12 to drill into them.

There are more complex versions of this approach, such as the "Reader" monad, but the above code is the simplest approach to start with.

Mark Seemann has a number of good posts on this topic, such as Integration Testing and SOLID: the next step is Functional and Ports and Adapters.

Upvotes: 9

Related Questions