8

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.

  • 1
    I for one don't understand this question. What does it matter if a function is pure for DI purposes? – Telastyn Oct 13 '17 at 16:21
  • So you propose that pure functions, impure functions and classes be injected all the same, no matter their size, transitive dependencies or anything. Could you perhaps elaborate on that? – DanielM Oct 13 '17 at 22:00
  • No, I just don't understand what you're asking. Functions are rarely injected into anything, and when they are, the consumer can't guarantee their purity. – Telastyn Oct 13 '17 at 22:46
  • 1
    You can inject every functionality, but often there is no value in mocking these functionalities in a test which means that injecting them has no value. For example, since operators are basically static functions, one could decide to inject the + operator and mock it in a test. Unless that has any value, you wouldn't do that. – user2180613 Oct 14 '17 at 16:29
  • 1
    Operators had crossed my mind too. It would certainly be inconvenient, but the point I was trying to find is how to make the decision of what to inject and what not. In other words, how to decide when there is value and when not. Inspired on @Goyo's answer, I think now that a good criteria is to inject everything which is not an intrinsic part of the system, to keep the system and its dependencies loosely coupled; and in contrast, just use (thus, don't inject) things that really are part of the system's identity, with which the system is highly cohesive. – DanielM Oct 14 '17 at 16:47
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)
  • Everything you said refers to pure functions, right? If so, what difference does it make with non-pure ones or classes? I mean, you could just hard code everything and apply the same logic ("everything I depend upon is already tested"). And a different question: could you expand on when would it be brilliant to inject pure functions and why? Thank you! – DanielM Oct 13 '17 at 21:57
  • 3
    @DanielM Absolutely - unit tests should cover some isolated unit of meaningful behaviour - if the validity of the behaviour of the "unit" is strictly dependent on returning the max of something, then abstracting away "max" is not useful and actually reduces the usefulness of the test, too. On the other hand, if I'm returning the "max" of whatever comes from some provider, then stubbing the provider is useful, because what it does isn't directly pertinent to the intended behaviour of this unit and its correctness can be verified elsewhere. Remember - "unit" != "class." – Ant P Oct 19 '17 at 21:21
  • 2
    Ultimately, though, overthinking this sort of thing is usually counterproductive. This is where test-first tends to come into its own. Define the isolated behaviour you want to test, then write the test and the decision as to what to inject and what not to inject is made for you. – Ant P Oct 19 '17 at 21:24
4

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.

  • Injecting the function does not mean that you have to test for function calls. Actually I would assert that the SUT does the right thing (regarding observable state) when it is passed the mock. OTOH moking a dependency that is not part of the SUT interface would be weird in my eyes. – Goyo Oct 13 '17 at 19:08
  • @Goyo, you will configure mock to return expected value only for particular input. So your mock will "guard" that correct input arguments were provided. – Fabio Oct 14 '17 at 11:19
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
    Injecting has nothing to do with object oriented programming. – Frank Hileman Oct 13 '17 at 17:40
  • @FrankHileman Better? You need dependency injection to depend on abstractions because you cannot instantiate them. – Goyo Oct 13 '17 at 18:04
  • It would be definitely beneficial to have a rough list of the most important factors involved in practice. Related to this, why wouldn't you care about coupling with max() (as oppose to something else)? – DanielM Oct 13 '17 at 22:21
  • SOLID is not "ideal" nor does it have anything to do with object oriented programming. – Frank Hileman Oct 13 '17 at 22:49
  • @FrankHileman How does SOLID not have anything to do with OOP? The five principles were proposed by Robert Martin in his year 2000 paper in a chapter called Principles of Object Oriented Class Design? – NickL Oct 14 '17 at 1:57
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.

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.