DEV Community

Cover image for This Is Why We Don't Test Private Methods

This Is Why We Don't Test Private Methods

Cesar Aguirre on February 03, 2025

I originally posted this post on my blog a long time ago in a galaxy far, far away. It's part of an ongoing series I've been publishing, called Uni...
Collapse
 
pengeszikra profile image
Peter Vivo

I think this is truly reflect, which is one weakest point of OOP concept.
Imho, a similar reason to avoide the OOP programming as possible.
Because even with the lightest object we are attach our functions to the data, and part of these data are protected also. So handling this data is bit painfull because always need to care about lifecycle of object not just the the data.

Collapse
 
siy profile image
Sergiy Yevtushenko

Data with operations defined on that data is not limited to OO. For example, FP monads indistinguishable from OO classes - data with operations defined on them. The only thing that actually matters is the immutability of data. As long as data immutable, one can consider private data a "context", i.e. few more input parameters to the function.

Collapse
 
pengeszikra profile image
Peter Vivo

You right. But when you attach operations to data, then you also faced to another problem, when that data is came from outer source then it need to be attach to operations, and when to send it, then need to detach from operations.

Thread Thread
 
siy profile image
Sergiy Yevtushenko

Serialization does not depend on OO or FP. There is no need to "attach" or "detach" methods either.

Collapse
 
canro91 profile image
Cesar Aguirre

That's a good point Peter. Everything lives inside an object. Thanks for your comment.

Collapse
 
joelbonetr profile image
JoelBonetR 🥇 • Edited

Think of a normal modern JS/TS codebase. E.g. any Next JS, Solid, Vue... whatever.
You'll probably find near to zero OOP, however you'll sure come across functions that are not exported, that act just as a helper of some other function, their reason to exist is solely to help other function within the same file.

You can, conceptually, refer to them as private functions, equivalent to a private method for all intents and purposes of this context. You won't be testing these, either.

As a rule of thumb, if the only reason to have the keyword "export" in a function is so you can import it for testing purposes, you're doing something wrong.

Easy way to explain it is that we don't test steps (functions/methods), we test the rigour of the algorithms (input A = output B) to ensure our software keeps working as expected, wether the algorithm has 2 or 95 steps is none of our business from the testing perspective.

This explanation is just to show an example of the same situation without involving the programming paradigm to avoid getting ourselves into the weeds here. These are two completely separated discussions.

Collapse
 
pengeszikra profile image
Peter Vivo

Don't forget the WebComponent where you need to be use OOP pattern.

But what is the deep concern of private method in OOP? Maybe we would like to show just a minimal public interface to the outer word, and protect that function to called from out side. So technically enough to test the public part of our Object. Under Object development maybe we set protected method to public, so we can write a test for that. Later if our object are stable, we can move that part to protect. Or we can use Symbol key for that function which we don't want to use public, but these Symbol we share to test, but not for in our lib.

Thread Thread
 
joelbonetr profile image
JoelBonetR 🥇

The method of Schrödinger 😂😂 No, don't do that, it just makes things more complicated for no reason. If you test all use-cases of your public functions/methods you're implicitly testing all the private ones at the same time and if not, the coverage will tell you exactly what you are missing.

Lastly I can't honestly consider web-components as part of the "modern" ecosystem. They're a standard API, yes, but a bad one nevertheless.

Thread Thread
 
pengeszikra profile image
Peter Vivo

If you don't use framework, then WebComponent will be really handy. I created a markdown-viewer WebComponet which is part of my game development process, and that is fine. I try to skip using JS framework. Maybe check out my game development: dev.to/pengeszikra/javascript-grea...

Thread Thread
 
joelbonetr profile image
JoelBonetR 🥇 • Edited

But... why? There are JS frameworks literally created with game development in mind. Phaser, Pixi, Babylon, PlayCanvas... Or even Three.js if you want to toy around at a lower level.
If it's for learning purposes it's fine but if you're serious with your development and want to achieve a production-ready product... Why reinvent the wheel?

Collapse
 
mirnes_mrkaljevic profile image
Mirnes Mrkaljevic

I think the most misleading point for developers is how they interpret the term "unit" in unit testing.
A unit is a "unit of work" and does not necessarily have to be a method in a class. Instead, a unit should be seen as a single behavior of a component towards the external world (in other words, its public interface).
Moreover, if the component adheres to the Single Responsibility Principle, writing tests is straightforward in most cases.

Collapse
 
canro91 profile image
Cesar Aguirre

Exaclty! Unit of Work != A single method

Collapse
 
siy profile image
Sergiy Yevtushenko

Determining if the component adheres to SRP is not straightforward, though. Much easier and at least as efficient is to follow the Single Level of Abstraction approach.

Collapse
 
manchicken profile image
Mike Stemle

Yeah, while I understand what you're saying, testing private methods only via their usage in other methods is poor practice IMO as it creates a paradigm where it is truly impossible to test all possible cases.

Especially in an paradigm which already encourages nearly-infinite code complexity, burying code in private methods and accepting that it will never be completely tested seems like malpractice.

Collapse
 
goodevilgenius profile image
Dan Jones

In my opinion, if you have a private method, and you can't test some part of that method from the public methods that call it, then you're not using that part of the method, and it should be removed.

There's not really a reason you wouldn't be able to test each part.

Collapse
 
canro91 profile image
Cesar Aguirre

I second this :)

Collapse
 
manchicken profile image
Mike Stemle

The part I think you’re missing is that those private methods are themselves their own function. In order to test the private methods completely, you’d need to have all possible conditions available in your public functions, and your tests would need to cover all conditions in both the public and private methods. The cyclomatic complexity can get pretty nasty pretty fast.

Another point against OOP, I guess.

Thread Thread
 
goodevilgenius profile image
Dan Jones

All possible conditions should be available from your public functions. If they're not, then those conditions are not possible, and can be removed from your function.

E.g., let's say I have something like this:

class Divider {
  public function int divide(int dividend, int divisor) {
    if (divisor == 0) {
      throw new Exception("Can't divide by zero");
    }
    return reallyDevide(dividend, divisor);
  }

  private function int reallyDevide(int dividend, int divisor) {
    try {
      return dividend/divisor;
    } catch (ArithmeticException e) {
      throw new Exception("Can't divide by zero");
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

In this example, the try/catch can't be tested, because divide already checks for a zero. Therefore, the try/catch is unnecessary. There is no code that calls it that can use it. So, you can leave it out, and change the second method simply to:

private function int reallyDevide(int dividend, int divisor) {
  return dividend/divisor;
}
Enter fullscreen mode Exit fullscreen mode

Now, there's no untestable code, because you got rid of the unnecessary code.

Thread Thread
 
thescottyjam profile image
theScottyJam

If you have two tests against a public method and two tests against a private one, and you change it so you instead test the private behavior through the public function, you still have a grand total of 4 tests. The quantity of tests don't have to increase because you're testing behaviors through the public API.

Also, if you really are trying to test each private function independently in order to have a low cyclomatic complexity, so you can fully test each thing, well, that doesn't really buy you anything. If you really think about it, every single operation and piece of syntax you use in the language is well tested - all you're doing is integrating those pieces together. But all of your bugs are going to lie in the integration. Similarly, if your unit of test is as small as individual private functions, well, each of those private functions will work wonderfully, but you're going to have lots of bugs in how they integrate unless you test larger chunks at once, even if they have high cyclomatic complexity.

Collapse
 
rhywynn profile image
Rhys

Not understanding the purpose of the code might be a reason to test every function. I'm not justifying it, but I could see that as a path of least resistance to ensure 'everything is tested', without the perceived overhead of designing comprehensive tests that cover every scenario.

Collapse
 
jared_williams_5785b12777 profile image
Jared Williams

If you can't reach all of your private methods from your public methods for testing, then you either have dead code, or need to reconsider the purposes of your methods.

Collapse
 
jankapunkt profile image
Jan Küster

This is why code coverage should be an essential part of unit testing. It enables to inspect those private parts and whether tests covered them, too.

Collapse
 
adic_threex profile image
adic threex • Edited

This is just a problem with legacy programming languages. In modern languages ​​(like Rust) you can access private identifiers in tests without breaking encapsulation. If a language forces you to avoid testing a part of your codebase simply because it has design flaws, this is a reason to don't use it in future.

Collapse
 
joan_pearl_b1036d7ba14b48 profile image
Joan Pearl

Thank, you but no. I will test my private internal methods without breaking access rights.

You can easily in modern c# use includes to import your test units as if they were private and internal to the class being tested without breaking encapsulation.

Collapse
 
stevsharp profile image
Spyros Ponaris

Thanks for sharing.. I think that we test fist behaviour, not implementation details. If private methods should be test, we must consider whether they should be extracted into their own class or exposed through a public interface.

Collapse
 
justintime4tea profile image
Justin Gross • Edited

Testing private methods indirectly, via assertions about the result or behavior observed from public method calls, also provides the advantage of plasticity. You can refactor your "internal" logic (private bits) and, provided you have thorough testing of the public methods, and they pass, your refactor will go much smoother with less coupled/spider-web/entangled changes. If the output/behavior doesn't change, from the perspective of calling a public function, it shouldn't matter how many or what specific private methods are called.

That being said, I prefer Rusts capability for testing "private" functions. You get the best of both worlds. Your "higher level" tests fail when "lower level" code fails but you get the specificity of what "lower level" is causing your failures without bashing your head against a wall chasing down your call chain. You simply write a test like any other and as long as the "private" method is in scope, it just works. No need for any workarounds, indirection or monkeypatching (looking at you JS/TS).

Collapse
 
htho profile image
Hauke T.

Hmm.

The way I read this article, the most prominent argument is: We don't test private methods **because* they are private.*
IMO this explanation does not give any reason at all.

For me, one reason to test private methods/functions is to make it easier to reason about them.
I want to write (and read) tests, that explain to me how a function behaves.
When a class transforms one complex state into another, I find I way too complicated to craft (and later read) the exact input states that lead to the expected output, where all edge-cases are covered. Instead I prefer to test the edge-cases at the private methods.

Whenever it's possible, I try to avoid testing private methods. But from time to time, It makes my code much easier to understand. And that is the second most important thing after writing code that does what it is supposed to do.

BTW: I wrote an article on how to test private methods in TypeScript.

Collapse
 
mae_marsh_ecb74f96470bb93 profile image
Mae Marsh

Your blog post really sparked my curiosity! I had never known that this topic could be this vast and informative. As we are working on Garnet ring, we want some information and your suggestion on this topic. For a brief detail what we are into, please visit out website. We will be waiting for your new blog and your feedback for us.