Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

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:

  • now my code is harder to follow because you can't just F12 (Go to Definition) into functions; this problem also exists with OO dependency injection but is mitigated by tooling (i.e. Resharper Go To Implementation).
  • The function I'm testing isn't technically the one called by production code (there could be errors in the mapping)
  • I don't even see a good name for that "Testable" version of the function
  • I'm polluting the module with duplicate definitions of everything

How do deal with these problems in functional programming?

like image 557
Asik Avatar asked Jul 29 '16 17:07

Asik


1 Answers

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.

like image 83
Grundoon Avatar answered Oct 04 '22 13:10

Grundoon