9

To extend a bit on the title, I'm trying to get to some conclusion about whether it is necessary or not to explicitly declare (i.e. inject) pure functions on which some other function or class depends.

Is it any given piece of code less testable or worse designed if it uses pure functions without asking for them? I would like to get to a conclusion on the matter, for any kind of pure function from simple, native functions (e.g. max(), min() – regardless of the language) to custom, more complicated ones that in turn may implicitly depend on other pure functions.

The usual concern is that if some code just uses a dependency directly, you will not be able to test it in isolation, namely you will be testing at the same time all that stuff that you silently brought with you. But this adds quite some boilerplate if you have to do it for every little function, so I wonder if this still holds for pure functions, and why or why not.

Whymarrh
  • 159
DanielM
  • 217

4 Answers4

10

No, it isn’t bad

The tests you write shouldn’t care how a certain class or function is implemented. Rather, it should ensure that they produce the results you want regardless of how exactly they are implemented.

As an example, consider the following class:

Coord2d{
    float x, y;

    ///Will round to the nearest whole number
    Coord2d Round();
}

You would want to test the ‘Round’ function to ensure that it returns what you expect, regardless of how the function is actually implemented. You would probably write a test similar to the following;

Coord2d testValue(0.6, 0.1)
testValueRounded = testValue.Round()
CHECK( testValueRounded.x == 1.0 )
CHECK( testValueRounded.y == 0.0 )

From a testing point of view, so long as your Coord2d::Round() function returns what you expect, you don’t care how it’s implemented.

In some cases, injecting a pure function could be a really brilliant way to make a class more testable or extensible.

In most cases, however, such as the Coord2d::Round() function above, there is no real need to inject a min/max/floor/ceil/trunc function. The function is designed to round its values to the nearest whole number. The tests that you write should check that the function does this.

Lastly, if you do want to test code that a class/function implementation depends upon, you can do so by simply writing tests for the dependency.

For example, if the Coord2d::Round() function was implemented like so...

Coord2d Round(){
    return Coord2d( floor(x + 0.5f),  floor(y + 0.5f))
}

If you wanted to test the ‘floor’ function, you can do so in a separate unit test.

CHECK( floor (1.436543) == 1.0)
CHECK( floor (134.936) == 134.0)
Ryan
  • 613
5

Is implicitly depending on pure functions bad

From testing point of view - No, but only in case of pure functions, when function returns always same output for same input.

How you test unit which use explicitly injected function?
You will inject mocked(fake) function and check that function was called with correct arguments.

But because we have pure function, which always return same result for same arguments - checking for input arguments is equal for checking output result.

With pure functions you don't need to configure extra dependencies/state.

As additional benefit you will get better encapsulation, you will test actual behaviour of unit.
And very important from testing point of view - you will be free to refactor internal code of unit without changing tests - for example you decide to add one more argument for pure function - with explicitly injected function(mocked in the tests) you will need to change test configuration, where with implicitly used function you do nothing.

I can imagine situation when you need to inject pure function - is when you want offer for consumers to change behaviour of unit.

public decimal CalculateTotalPrice(Order order, Func<Customer, decimal> calculateDiscount)
{
    // Do some calculation based on order data
    var totalPrice = order.Lines.Sum(line => line.Price);

    // Then
    var discountAmount = calculateDiscount(order.Customer);

    return totalPrice - discountAmount;
}

For method above you expects that discount calculation can be changed by consumers, so you have to exclude testing it's logic from the unit tests for CalcualteTotalPrice method.

Fabio
  • 3,166
4

In an ideal world of SOLID OO programming you would be injecting every external behavior. In practice you will be always using some simple (or not so simple) functions directly.

If you are computing the maximum of a set of numbers it would be probably overkill to inject a max function and mock it in unit tests. Usually you don't care about decoupling your code from a specific implementation of max and test it in isolation.

If you are doing something complex like finding a path of minimum cost in a graph then you'd better inject the concrete implementation for flexibility and mock it in unit tests for better performance. In this case it might be worth the work of decoupling the path finding algorithm from the code using it.

There cannot be an answer for "any kind of pure function", you have to decide where to draw the line. There are too many factors involved in this decision. In the end you have to weight the benefits against the troubles that decoupling gives to you, and that depends on you and your context.

I see in the comments that you ask about the difference between injecting classes (actually you inject objects) and functions. There is no difference, only language features make it look different. From an abstract point of view calling a function is no different from calling some object's method and you inject (or not) the function for the same reasons you inject (or not) the object: to decouple that behavior from the calling code and have the ability to use different implementations of the behavior and decide somewhere else what implementation to use.

So basically whatever criteria you find valid for dependency injection you can use it regardless the dependency is an object or a function. There might be some nuances depending on the language (i.e. if you are using java this is a non-issue).

3

Pure functions do not affect class testability because of their properties:

  • their output (or error/exception) only depends on their inputs;
  • their output does not depend on world state;
  • their operation does not modify world state.

This means that they are roughly in the same realm as private methods, which are completely under the control of the class, with the added bonus of not even depending on the current state of the instance (i.e. "this"). The user class "controls" the output because it fully controls the inputs.

The examples you gave, max() and min(), are deterministic. However, random() and currentTime() are procedures, not pure functions (they depend on/modify world state out of band), for example. These last two would have to be injected to make testing possible.