Private, a lib to test private functions in 2022

I definitely agree with splitting into modules in your example, but there are also examples where I wouldn’t do it.
I’m talking a situation where there is complex behavior + something that’s really tightly coupled to something else. I don’t have a good example right away because it’s not something that comes up that often.
But every now and then, I want to test some private functions. But because a lot of languages don’t really have decent support for that (some do), I mostly don’t bother and either make it public or extract it. Even though it’s an implementation detail - that I’d rather not expose to everyone.

I also think we have these kind of discussions because most languages only have private or public functions. That’s what I like about the idea of boundary. You don’t need to make things public for everyone, you can say who is allowed to call what. So, by making something public for testability reasons, you don’t need to expose it to the whole world.

Elixir itself and many libraries do that with @moduledoc false and public functions. Yes someone can still call those functions, but they’re excluded from many places like docs, autocomplete, ….

2 Likes

Exactly, and tight coupling is a design smell :slight_smile: so when I find myself in this situation, if I can, I try to uncouple instead of testing implementation details.

I agree, to me this isn’t really about public vs. private and not even about Elixir. It’s about desgin of code and tests. When I see “private” my brain reads “implementation details”. All I’m saying is taht I really, really try to avoid testing implementation details if possible.

I’m going to drop this talk about coupling and cohesion: https://www.youtube.com/watch?v=3gib0hKYjB0
A great talk in my opinion.

I’d like my implementation details to also be correct :wink:

ha! If your implementation details are flawed and nobody from the outside can notice anything, it means that the flaw doesn’t matter, does it? I’m lazy, in that case I say: “who cares” :wink:

That’s a no, good sir.

The poor guy who comes after you that has to maintain the duct tape and fencing wire implementation :wink:

End users aren’t the only users of the code. As @dimitarvp notes - human readability and comprehensibility is a primary concern for sustainable software, and that often involves demonstrating intermediate results (that aren’t of concern to an end user) are correct.

2 Likes

My God! This is what you got from what I wrote so far? Ok, I’ll summarize:

  • break down your codebase into components/module/classes with clear interfaces and intended behavior
  • test the behavior of these components via their interfaces/public APIs
  • refactor implementation details until you have code that you like
  • if you find yourself wanting to test implementation details of a component/module/class, revisit your design. Maybe the component is trying to do too much, and you can break it down further. Maybe you forgot an aspect of the behavior of the component, which you can now distill into a test

The correctness you should care about is defined by the behavior of your module. If it’s not visible in the behavior then it’s an implementation detail. Don’t couple your tests to these details: you’ll end up with code that is hard to change. Behavior of a module ideally shouldn’t change, implementation details should be able to change: that’s refactoring. The duct tape arises often because people couple things too tightly into a mess which nobody wants to unravel. Testing only behavior and not internal details reduces coupling, and helps make refactoring easier.

So… no mention of “end-user” in the above. I hope it’s clearer now :pray:

P.S: I think that maybe the above seems so obvious to me because I practice TDD whenever I can. I write tests before writing the implemetation, so obviously there are no or very little implementation details I can couple my test to. I imagine (and recall from years ago) that without TDD, keeping your tests separate from the implementation must be harder.

3 Likes

I was being a bit flippant in response to a single post, but thanks for the clarification, and it’s a good end state to head towards.

This is also an excellent thread to challenge and refine our own practices, so thanks to everyone for that.

I am with @ityonemo on other reasons to test, and at times, yes, I absolutely want harnesses around parts of an implementation detail:

Development of non-trivial algorithms often needs check-points, clarification and verification at a lower level than you would want to expose through public/module interfaces.

Hence “no absolute rules” :grin:.

2 Likes

No, absolutely no absolute rules :slight_smile:

Just good practices to keep in the back of your mind

When I’m writing anything remotely interesting, I tend to spend a lot more time “under the hood” than I do working on external API. While I agree in principle that the external API is what matters, in practice, developing new features and only testing them through the external interface is dramatically slower than just testing those things directly. That’s exacerbated by the fact that external API is often messier/stateful compared to a functional core that no one may ever interact with directly.

I suppose that once you’re done, you could delete all those tests that you wrote while building the new thing and trust that the details are sufficiently tested through the external API, but that would seem a bit wasteful. :slight_smile:

2 Likes

Tests are also code that you need to maintain. Delete tests that don’t have any value anymore. Nothing wasteful about that.

6 Likes

I agree with @tcoopman here, some tests do eventually outlive their usefulness and should be deleted at one point.

1 Like

Another +1 for throwing away such tests. I often treat TDD like REPL-driven development where instead of having to press up a bunch of times and then enter to re-run the test, I have it in a file and can just press one key-combo to run it. But it doesn’t necessarily mean that I need to keep that around once things are working if it’s low value. That said, I’ve definitely felt attached to those tests before (and it still happens).

I also find having mountains of unit tests to incredibly daunting and I have a feeling that that is what contributes to a lot of anti-testing sentiments. I strive to only have tests that describe business value because I actually want to read them. If you have lower-level unit tests that always pass and are covered elsewhere anyway, just throw them away. As mentioned, there is a cost to all code that is kept around.

More on topic: I don’t really want to head-to-head with pragdave, but the module thing he is describing is similar to what I was describing. Since I’m pro-dynamic programming, even if there are some semblance of types, I feel that some social contracts in code are OK—like, if there is a GenServer API available then obviously you should always use that, even if the impls are public! That’s maybe an unpopular opinion, though.

And of course, to reiterate what has been said numerous times, there are no set rules.

I say this all the time and there’s no way any of us here will ever live to see the day where there would be set rules (if that’s even possible). I always that I wonder how many hundreds of years people argued over whether a rock or a heavy stick made a better hammer before the modern hammer was standardized.

2 Likes