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

One interesting thing that is easy to notice about all of the examples of in the article is that they are absolutely infested with objects.

I don't have anything against objects, per se, but I think they tend to make unit testing much more difficult to accomplish. The closer your code resembles pure functions, the easier it is to do dependency injection and unit testing.




There isn't pure functions the moment you touch any kind of IO.

Plus the same problem arises with modules instead of objects, which traditionally are even harder to customize.


> There isn't pure functions the moment you touch any kind of IO.

You can get pretty far with good abstractions and dependency injection. Go's io::Reader and io::Writer interfaces are a great example of this. The resulting functions aren't pure in a technical sense, but they're pretty easy to unit test none the less.

> Plus the same problem arises with modules instead of objects, which traditionally are even harder to customize.

Maybe you could elaborate. I really don't understand what you mean here.

From what I understand, modules just scope names, they don't maintain state. I don't see how they have the same problems as objects.


> You can get pretty far with good abstractions and dependency injection

Which goes back to the article's point of having to write code that is unit test friendly.

Now architecture decisions have to integrate interfaces that wouldn't be needed otherwise.

> Maybe you could elaborate. I really don't understand what you mean here.

Modules keep state via global variables, module private functions and the surface control that they might expose via public API for the module.

Additionally on languages that support them, they can be made available as binary only libraries.


> Which goes back to the article's point of having to write code that is unit test friendly.

> Now architecture decisions have to integrate interfaces that wouldn't be needed otherwise.

You're not wrong.

But in the context of functions, that doesn't seem to me to be particularly onerous. If the worst I'm forced to do is change the type of my parameters to an interface instead of a concrete type, that seems like a pretty small price to pay for easy testability. Certainly a much smaller price than the examples in the article.


You are assuming that interfaces exist as language concept.

Imagine doing unit tests for a C application, where modules == translation unit/static/dynamic library, thus you can only do black box testing.

Now one needs to clutter it with function pointers everywhere, or start faking interfaces with structs, just for the benefit of unit tests.

And with static/dynamic libraries than one might need to start injecting symbols into the linker to redirect calls into mocking functions.

All just to keep QA dashboards green.


That's how a lot of great C code is written anyway. A C library should abstract out logging, allocation, and IO so that the client code can change them out if need be.

The fact that it makes unit testing easier is just icing on the cake.


Having written tests for enterprise C code, I wouldn't call it a great experience, rather something I am glad not to ever repeat again.

Mainly due to the linking hacks and low level debugging sessions required to mock all necessary calls.

Plus that was just an example, there are plenty of languages with modules and binary libraries.


I mean, that's why great libraries don't make you do that. There's a lot of crap libraries.

The libraries dependencies should all be indirected through whatever context struct you pass to all your calls.


Agreed, and this goes back to the initial thread that just because a language is more focused on functions it doesn't make testing automatically better, unless it was written with testing friendliness as part of the requirements.

Sadly not all code is great.


For C, I've found it's not a test friendliness thing though; the great C libraries were doing this before unit testing made it's way into their codebases. They dependency inject IO, memory allocation, and logging because they have no idea what you as the end user are going to be using for those. So you pass all that in on an env struct when you initialize the library.

You probably want it rigged up to your own logger instead of just blindly writing to stdout. You probably want the library's allocations tagged somehow on the heap so you can track down memory leaks. You probably don't want it doing IO directly, because of how many different way there are to do IO.

It's all more a function of how incredibly varied c envs are, than design for testability. It just happens to be very testable as an aside.


Yes there is. What you do is have don't put any IO calls inside your pure functions, but rather pass in their results as parameters.

Keep the impure code and the pure code separated.


Beautifully said, practically impossible unless the language imposes it as programming model and you have 100% control over the complete source code.


Yup, this is very true. As long as you return something deterministic, unit testing is easy.

It's where you need to handle mutable state with objects that things get trickier.

Unfortunately, these are exactly the places where you most need tests.

I'm a big fan of constantly returning things rather than holding state in objects, for specifically this reason.


A basic issue of encapsulation: objects shouldn't rely on the mutable state of something else, but only on their own.


> The closer your code resembles pure functions, the easier it is to do dependency injection and unit testing.

If the only thing you inject is data, can we still call that "dependency injection"?


> If the only thing you inject is data, can we still call that "dependency injection"?

I suppose that's a philosophical question.

Probably the 2 most common functions I 'dependency inject' are rand() and time.now(). I feel like they count, but you might not.


I mean, if you pass a HOF to some other function, then that is also a dependency.


Correct, but I haven't seen it happen often in practice. I mean, pretty much every project uses HOF, but few have many of them.

I also tend to avoid HOF when I can instead pass data around explicitly.


You're one sentence away from discovering Common Lisp ; )


Define data.


Anything that doesn't have an arrow in its type.


Mock frameworks make it trivial.

  Foo foo = new Foo(mock(Bar.class))

  foo.a(x,y,z)
Not sure what the issue is there really.


> Mock frameworks make it trivial.

In my experience mock objects can be brittle. A few sprinkled in judiciously can be ok, but once the density gets high enough, it starts to feel like the test becomes decoupled from the actual code it's supposed to test.


Agree. To add to this, many unit tests that you might have to do become obsolete with a strongly typed functional language. At that point you’re basically only integration testing the API boundaries / external interfaces.




Consider applying for YC's Spring batch! Applications are open till Feb 11.

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

Search: