How to make this function suitable for testing?

I have a function responsible for collecting bundles of configurations and creating a larger configuration from all of these parts. So this is 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. } 

Currently, I have unit tests for getThis , getThat , createThatThing , but not for applyUpdate . I don't want to re-check what getThis , etc., I just want to check the logic related to applyUpdate , and just drown out getThis . In an object-oriented style, they will be passed through the interface through dependency injection. In a functional style, I'm not sure 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 like the functional equivalent of Bastard Injection, but beyond that, I'm mostly worried:

  • now my code is harder to follow because you cannot just F12 (Go to the definition) in a function; this problem also exists OO, but is mitigated by tools (i.e. Resharper Go Implementation).
  • The function that I am testing is not technically called production code (there may be errors in matching)
  • I don’t even see a good name for this "test" version of the function
  • I pollute the module with duplicate definitions of everything

How to cope with these problems in functional programming?

+8
unit-testing functional-programming f #
source share
2 answers

You said:

In an object-oriented style, they will be passed through the interface through dependency injection.

And the same approach is used in FP, but instead of entering through the constructor of the object, you "enter" as the parameters of the function.

So, you are on the right track with your applyUpdateTestable , except that it will also be used as real code, and not just as validated code.

For example, here a function with three additional dependencies is passed to:

 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 enter the real dependencies:

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

or more simply using a partial application:

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

and in the test version, you enter mocks or stub instead:

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

In the "production" example above, I statically hardcoded the dependencies on Real functions, but just like in the case of OO style dependency injection, the production of applyUpdate can be created by some top-level coordinator and then passed to 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 dependencies, you can still use F12 to drill them.

More complex versions of this approach exist, such as the Reading Monad, but the code above is the easiest way to start with.

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

+9
source share

Scott's answer (@Grundoon) covers a more direct translation from OOP to FP. This is suitable if you expect one of the getThis functions, getThat be unclean.

In general, passing functions as arguments to other functions is a fairly functional thing (then the receiver function is called a higher-order function), but this should be done in the interest of variability. Adding additional function arguments for testing purposes only leads to what David Heynnemeyer Hansson calls the damage caused by testing .

In this answer, I would like to offer another perspective, although I want to emphasize that Scott's answer coincides with my own thinking (and that I supported it). It is suitable for F # because F # is a hybrid language and implicit impure functions are possible.

However, in a strictly functional language (such as Haskell), pure functions are by default. If we assume that getThis , getThat , etc. All are referentially transparent (clean), function calls can be replaced with their return values.

This means that you should not replace them with a double test .

Instead, you can simply write your tests as follows:

 [<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 can argue that this test only duplicates the production code, but it would also be possible to conduct a test using double tests like OO. I assume that the real problem is more complicated than the OP, which does not really guarantee validation of the applyUpdate function.

You can also argue that this test is not a unit test, and I would agree on semantics; I call these tests Facade tests .

Pure functions are internally verifiable , so there is no reason to change their design to make them "verifiable."

+3
source share

All Articles