DEV Community

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

Posted on • Originally published at canro91.github.io

This Is Why We Don't Test Private Methods

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 Unit Testing 101.


Trying to test private methods causes a lot of confusion.

That's a common question we all have made when finding unit testing for the first time. These days, I found that very same question on Reddit:

Can someone explain to me why unit testing our private methods is bad?

And here's my full answer:

Because we don't want to break encapsulation.

If you have a private method, how are you going to call it from a test class or method?

It's private. You can only access it from inside the same class. That's the whole point of access modifiers: restricting access to the fields and properties that hold the internal state of a class.

And please don't make your private methods public and static to call them directly inside unit tests. They're private for a reason, right? We don't want the rest of your code to use them directly.

Exposing internals is the most common mistake when writing tests. I've seen it and fixed it before.

Let's take the HasAdmin() method from the question as an example,

private bool HasAdmin(List<string> relations, bool hasPermission)
{
    // Beep, beep, boop...
}
Enter fullscreen mode Exit fullscreen mode

Unless HasAdmin() has 0 references—if that's the case, you should remove it—another method from the same class is calling it. And you can trace the chain of method calls back to a public method.

HasAdmin(), or any other private method down in the chain of method calls, is changing something that you can observe from public methods. Probably it's affecting a return value or changing an internal state you can inspect with getters. That's what you should test instead.

To test HasAdmin(), create a User object with the right relations and permissions, call the public methods you have, and check what should change when your user is an admin or not. Maybe you return additional data only admins can access or finish an action without throwing a UnauthorizedAccessException.

You test private methods indirectly while testing the observable behavior exposed through public methods.

Et voilà!


Download your free copy of my ebook, Unit Testing 101: From Zero to Your First Tests. It covers all the basics to help you write your first unit tests in C#. Plus, get 5 bonus lessons delivered straight to your email to make your first tests even better.

Top comments (50)

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
 
chrisco484 profile image
Christopher Colemani • Edited

Real world objects have both functionality (you can tell it to do something by calling one of its functions) and state (it stores data as attributes and relationships to remember what state it is in).

Real world objects also have lifecycles - we can try to simplify an application by pretending that real world objects don't have lifecycles but then you would be breaking Einstein's simplicity theorem which goes something like "Make things as simple as possible but simpler" i.e. not too simple that it's no longer correct!

Assuming objects are 100% immutable may satisfy our puritanical mathematical desires and fit nicely into "set theory" but it's just a theory that's best kept for the theoretical mathematics lectures at university - meanwhile, back in the real world, we need to cope with the fact that objects aren't always immutable and so we need to be able to write software that can represent thousands or millions of real world mutable objects and the interactions between them.

OOP allows you to encapsulate the functionality and the state of a real world object inside a software object that is defined by a class. It also allows incredible amount of reuse via inheritance (making sure, of course, that the correct balance of inheritance vs composition is always achieved).

In other words OOP is the most programatically efficient way to code if you want your software objects to be as close as possible to the real world objects that they represent.

Many developers love this concept and have discovered that OOP's ability to model real world objects via encapsulation, inheritance and polymorphism means that as their applications grow the complexity of the source code grows close to linearly instead of exponentially as with most other paradigms that came before and after it.

Collapse
 
pengeszikra profile image
Peter Vivo

It is sounds good, but at the end of the day we are combined and inherit of some class, where we are hope ( have hidden part ) to solve our problem, but in a real world object never same as the picked one. Or in other example: we just want to make a documentation with code example, and some tricky reason - company are mandatory use excel and word for work. So we pick a word to make our document. Which is unreadable ( don't count how professional we are on word use ) because word separated our document to unified page sized, because that program is depend on old print to something to paper. Now we are try to save our planet and don't use paper for every uninformative documentation ( I hope ), but we are still using word. Why? because we just want a banane but get the whole jungle.

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?

Thread Thread
 
pengeszikra profile image
Peter Vivo

I am big fun of Mr. Doob. But Three.js for example are created 2010 so that is based on OOP pradigm, Phaser also. Each of these game engine are based on WebGL. But my initial ida is: the simple vanila HTML/CSS is much more useful to represent card game in 3D. Because on WebGL really hard to make a well formated text on 3D plane. Also problematic to make a simple card which rounded corner.
Please show me another card game, where you saw the deck as real 3D object maded by cards.
So that why I spend work to create a this way.
The state handling is also important to every complex game flow handling. My previous game used by react have much harder state management, compared to this one. Where I can use a one simple function with Proxy to handle my states, and that is working perfectly, because that state don't attach directly to a view as in React. So I can use my code even on server side also.
My last goal is to compare how large or small code/css need to be make for creating a this type of game.

Summary: I would like to pushing forward our way of working and thinking.

Thread Thread
 
joelbonetr profile image
JoelBonetR 🥇

Please do create a new post with your discoveries and comparisons, I am looking forward to reading them all! 😁

Thread Thread
 
pengeszikra profile image
Peter Vivo

thx, I will

Collapse
 
mirnes_mrkaljevic profile image
Mirnes

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
 
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
 
canro91 profile image
Cesar Aguirre

Exaclty! Unit of Work != A single method

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
 
jeffrey_tackett_5ef1a0bdf profile image
Jeffrey Tackett

I was going to point this out as a rebuttal to this article, but since you already pointed it out, I will throw my hat into this ring as well. My way of viewing the problem is that public interfaces are for teams using your services through your team's API, but your team should be able to test whatever code is needed to be tested and private testing usually devolves into a lot of mocking and now you have to ensure that those mocks are updated anytime the signature of the function/method changes, which happens more frequently since it isn't exposed to anyone outside of the team and isn't considered a contract between consumers and providers.

I find it ridiculous to limit developers working on a code base to the same standards as those that are using the services it provides. I would caution that you must have tests for the expected behaviors of the public interfaces and that these tests should cover all scenarios that are documented, which often is a much shorter list than the business logic needed to create the output.

Collapse
 
trevor_price_48e5ffbce1f9 profile image
Trevor Price

Agreed. My private methods often have complex business logic, and you can bet I will want really thorough test coverage on it.

Collapse
 
peter_lamb_fa3fc2089b698c profile image
Peter Lamb

Then you probably need to consider refactoring your code, if you have private methods that are so complex that you feel they need to be tested using unit tests. The real problem here is not the perceived need to unit test private methods, but the code itself.

Thread Thread
 
trevor_price_48e5ffbce1f9 profile image
Trevor Price

Yeah, probably right. Truly skilled devs write code so gloriously simple that Selenium tests should be able to fully validate an entire web app stack.

Thread Thread
 
peter_lamb_fa3fc2089b698c profile image
Peter Lamb

No, good devs don't write private methods, nor public methods for that matter, that are ludicrously complex. They refactor them so that they are more modular and solve simpler problems. They refactor large classes into smaller classes, smaller methods on a constant basis.

If a dev feels they need to test private methods, then that should be a code smell to them, not a rallying call for writing tests for private methods. It indicates a potential issue with the complexity of the code, or that one method or module is doing too much. Private methods are always testable via public ones using unit tests. To be clear, I am NOT saying that private methods should not be tested, but unit testing is simply not the right mechanism for it. They are for public interfaces of code modules and that simply does not include private implementation details of the unit.

Thread Thread
 
trevor_price_48e5ffbce1f9 profile image
Trevor Price

This feels like it's verging on religious dogma now

Thread Thread
 
adic_threex profile image
adic threex

Testing concretes logic placed in private method is always be accuracied than testing all logic from public method.

The problem is there only public and private with only 'flat' scope. Incupsulation in Rust much more better because you can have nested modules which can be public for theit parent but private globally. There no difference when some logic decrared as private method in current module or as public method in child private module.

Collapse
 
canro91 profile image
Cesar Aguirre

Didn't know that about Rest. Thanks for sharing!

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
 
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
 
canro91 profile image
Cesar Aguirre

I second this :)

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
 
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
 
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
 
canro91 profile image
Cesar Aguirre

Wow! This is the second time I read in this thread about Rust and testing private functions...I'm curious now. Thanks for your comment.

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
 
chema profile image
José María CL

I guess this is a "it depends" type discussion.

I think the purpose of unit testing is to ensure that a planned and necessary behavior works as we define it. It's quite possible that there are some bugs even with unit testing because we probably don't consider all real scenarios, but we can cover them if they appear.

That said, I also think that some parts of the code don't need to be tested, only the exposed functions, only the "views" (internal APIs, helper functions, application services, infrastructure services)
E. G.


// I want to ensure this function return expected results according the defined requirements, and fails with unwanted behaviors
export const getFullNameFormatted = ({ firstName, lastName }) => {
  const fullName = [ firstName, lastName ].filter(Boolean)
  return formatNameCase(fullName)
}

// I dont really need to test this piece. It could help, but in this exact case maybe doesn't add value. This is a "it depends" part
const formatNameCase = (fullName) => {
   return `🤠 *${fullName}* 🐮`
}


Enter fullscreen mode Exit fullscreen mode
 
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
 
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.

Some comments may only be visible to logged-in visitors. Sign in to view all comments.