Hacker News new | past | comments | ask | show | jobs | submit login

> Honest question: if you don't do dependency inversion, or if you don't depend on interfaces/abstractions that can't be mocked - how do you unit test your code?

The author isn't actually demanding you change your tests (unless they are unnecessarily complicated).

> Unit testing is the only reason pretty much all of my code depends on interfaces. Some people seem to consider this a bad thing/over-engineering, but it's how I've seen it done in every place I've worked at.

Unit tests require 'seams' to divide the tested code into units but those seams don't have to be interfaces. Some languages don't have interfaces at all - Ruby, say - and they unit test just fine. The pattern of every Foo having a corresponding IFoo is a bad thing - you should replace IFoo with smaller role interfaces. Having an IFoo if it isn't necessary to test Foo and there's only one implementation is indeed over engineering.

> I wish to know this as well. And what about functional programming languages?!

Unit testing's much the same in functional languages, though it tends to be easier because pure functions are easier to test. But how code gets its dependencies tends to differ a bit. It may help to think of dependency injection as parameterisation. Or, perhaps, parameterisation where you don't use the parameter immediately.

Suppose I want to test a function foo that depends on another function bar:

    fun foo() {
        return bar()+1
    }
Well with objects and interfaces, I can do this:

    interface IBar {
        bar()
    }

    class Bar(): IBar {
        fun bar() { return 2 }
    }

    class Foo(val bar: IBar) {
        fun foo() {
            return bar()+1
        }
    }
And then I can make a test double for Bar in my test, and when I invoke foo, then it will call the bar of my test double.

Now, of course, in this example, I could test with the real Bar and I don't need a test double. That's partly because the example is simple but also because foo and bar are pure functions. But let's ignore that and see how we can do the same in a functional language.

    fun foo(n: Int) {
        return bar()+n
    }
The question is, how can we control the value of bar in our tests?

The simplest answer is that, since bar is varying (between test and prod), then bar is a parameter:

    fun foo(bar: ()->Int, n: Int) {
        return bar()+n
    }
Now the reason we don't do this in OOP languages is that we won't have bar at the call site. That is the production code calls foo like this:

    foo(n)
and not like this:

    foo(myTestBar, n)
That's why our test code looks like this:

    val myFoo = Foo(myTestBar)
    assert(myFoo.foo(3)).isEqualTo(something)
though, if foo takes its dependency as a parameter, the test could just look like this:

    assert(foo(myTestBar, 3)).isEqualTo(something)
But the prod code won't look like that, it only passes n. So we need to pass bar before that. That's why we pass bar to the constructor in the OOP version.

OOP languages generally expect you to pass all the parameters to a function when you call it but some functional languages don't require this. That is, you can pass bar on its own - a mechanism known as 'partial application'.

You'd use it like this:

    // Do this when 'wiring up' the application
    val myFoo = foo(myTestBar)

    // now I only need to pass n
    myFoo(n)
If your language doesn't provide partial application, then you can do the same with a function that captures the dependency:

    // Do this when 'wiring up' the application
    val myFoo = {n-> foo(myTestBar, n)}
    
    // now I only need to pass n
    myFoo(n)
Now, notice that, since bar is just a function (of type ()->Int), I didn't need to create a separate interface IBar. For this purpose, functions "just work". They are equivalent to interfaces containing a single function (Java's SAM recognises this, if that's familiar to you).

Notice also that an interface of a single function is as small as it can be. Often, we'll see a class Bar with, say, ten methods, and a corresponding IBar also with ten methods.

SOLID's Interface Segregation Principle says to prefer small 'role' interfaces over obese interfaces such as the ten-methods IBar. And, of course, if you're using functions, that happens naturally.

Hope this helps.




It does thank you, learnt a bit about partial application when dabbled with Haskell, tho never never really got close to Monad stuff (io I guess?!). Now I'm trying to learn more about Rust also I identified references about "Working effectively with legacy code".

My point being, do you have more references for studying about tests?! As a NodeJs and Python developer today (with almost no tests) it's really hard work with these legacy code :|


Honestly, I think "Working Effectively with Legacy Code" is the main work in this field.

If you can pair with someone who habitually works TDD, do so. Sounds like getting experience in a team with effective modern practices (see "Accelerate" by Forsgren et al) could change your life for the better.




Guidelines | FAQ | Lists | API | Security | Legal | Apply to YC | Contact

Search: