2

Advice on unit testing is often focused on publicly exposed classes. What about cases where the only public class is simply there to instantiate internal classes? Should the internal classes be unit tested, even if they are not exposed to the outside world?

My intuition tells me that the internal classes are doing the meaningful work and should be tested, regardless of the semantics.

10
  • The idea is that all the meaningful work done by the internal classes can be tested via operations on the publicly exposed classes. If a public class 'only' instantiates an internal class, then really what is it doing? It can't return that object, because it's internal, there must be methods on the public thing which interact with the object. If it's purely side effectful, then yes that's hard to test, but you should test the side effects, if you can't refactor.
    – Jovash
    Jan 26 at 18:38
  • Thanks Jovash. I run into this type of thing when writing plugins that call public methods from a referenced API dll. My public Run method is an override from the API that only returns a bool on success, but the internal classes are calling all of the sealed API class methods. Jan 26 at 18:51
  • 1
    theOnlyPublicClass.instantiateInternalClasses(); and then what? If nothing goes in and nothing comes out there is nothing to test since this does nothing. Jan 26 at 18:58
  • The host application is executing the public methods called in my app, but provided by the host API. In other words, I may not have public methods, but the API I am referencing does. Jan 26 at 19:01
  • OK so the host isn't just instantiating internal classes. It's also calling their methods. Still looking for this "meaningful work". What happens when the methods are called? Not inside. What happens outside? Jan 26 at 19:10

3 Answers 3

2

Should the internal classes be unit tested, even if they are not exposed to the outside world?

  • (a) Do we make mistakes when implementing internal classes; especially mistakes that risk significant loss of property?
  • (b) Is is acceptable to ship implementations of internal classes that risk significant loss of property?

When the answers to these questions are commonly (a) yes and (b) no; we will usually want to make some investment in risk mitigation.

Which investment(s) we want to make, and whether or not "unit testing" is an appropriate label for some subset of those investments -- that's a messier question.

Code that is complicated and changing often certainly needs some cost-effective mistake detection strategy; fast/cheap automated checks that assist the programmer in verifying their changes is likely to be a good idea to try.

1

I think any possible mixture of opinions here might stem from how we perceive "public" vs. "internal". In my particular case, "public" encompasses a very narrow subset of our software development kit's functionality and "internal" a massive superset of functionality - including very generalized functionality used from various public functions and types -- so it's absolutely crucial that we write unit tests (as well as integration tests) for the "internal" functions and types.

Of course if all you have is an internal "helper" class used in one place -- and perhaps even guaranteed to stay this way (ex: a nested private class like a node for a tree as a basic CS example) -- then I think it's productive only to test the public interface of the public class. There you have a distinction not just between "public" and "internal" but between "interface" and "implementation detail". It can quickly work towards counter-productivity to try to handle edge cases that can never be encountered in implementation details through the constraints of the way that such internal functionality is guaranteed to be used by only one place.

Especially for large-scale designs, you generally can't afford to perfect every implementation detail to handle every possible use case that's a massive superset of the actual use case needed of the public interface. What's most important in a large-scale system is to make sure your interfaces consistently produce expected outputs for a variety of given inputs (as wide a variety as needed to test every unique branch of code). It can also go against the idea that implementation details should be free to change without breaking any external dependencies, since changing the implementation details might break those internal unit tests even when the interface still conforms to the identical input and output requirements. So I'd generally avoid unit testing anything that can be considered an implementation detail to keep them more free and flexible to change.

The perfectionist in me usually wants to handle all those edge cases that can never presently manifest even in implementation details guaranteed only to be used by one place but the need for productivity often calls for YAGNI (I compromise by sprinkling lots of asserts to make sure the internal implementation detail/helper class isn't used in a way it isn't supposed to by the public class). If the implementation details can't handle some hypothetical use case that will never manifest given the constraints of the public classes and the way they use those implementation details, then I think it can only be argued from a business standpoint that we're wasting time if we're trying to test for a problem that will never (or unlikely to ever) manifest.

BTW: Apologies if this is a dumb answer. I wanted to write it in a comment but I lack the rep.

0

Should the internal classes be unit tested

Yes, definitely!

Let's take a step back. Why do we spend time writing automated unit tests? To save us time in future. Inevitably there will be a new feature, a bugfix, a refactor. And after the dust settles and the code's committed, we will want to verify that stuff still works, with no regressions. Minimally, we have to at least do that for the happy path.

Cardinal rule: A line of code that never ran is probably a line that is buggy.

So we want to measure code coverage and write more tests until we believe maintenance risk has been adequately mitigated.

I don't particularly care whether we call an automated test "unit" or "integration", at least not initially. My chief concern is to have a test exercise the code, so Red source lines in the coverage report turn Green. A test that comes straight from a User Story is likely to be a relatively high level integration test, and it might suffice to adequately mitigate risk.

With some testing in place, it's time to look at the uncovered Red lines and ask how we might exercise them. Perhaps there's an if looking for two values and only a single input value has been provided so far. Great! Add a tweaked test case which exercises the else, verify the line turned Green, and move on.

But sometimes a conditional or exception is "impossible" to exercise, it is a "cannot happen" case, at least after functions higher on the call stack have sanitized inputs. In those cases, definitely write an isolated unit test which exercises all the cases. Also prefer unit tests for functions with multiple callers, in case requirements for one of the callers changes in future. If you ever find that it's non-trivial to figure out why a big integration test failed or how to fix it, definitely break out simple unit tests. If an integration test takes a "long" time, again it's time to break out unit tests or rely on test double techniques such as mocking.

If a source line stubbornly remains Red, and is "hard" to cover, consider whether it is even needed in production. Something that is never called might be something we can safely delete.

Now, back to your original question.

Write an automated test that directly corresponds to a User Story. Likely it is a high level integration test. Examine the coverage report, and write additional integration or unit tests as warranted. Lather, rinse, repeat.

At some point you will see some uncovered code that you want to access but it is "hard", perhaps because it is private. In a language like python you should not hesitate for a moment, just call the private _helper() -- that's a valuable test. In a language that prohibits such access, you'll have some choices to make. Your test might need to be a friend, in the same module, or otherwise close enough to the target code to allow access. You might promote the target method, or add a public method, solely to support testing. You might reconsider your public API, for example by adopting Dependency Injection. There are many possibilities, but at the end of the day you need an automated test suite that brings maintenance risk down to what you find is an acceptable level. So yes, test the internal classes, one way or another.

1
  • Thanks J_H. This is in line with my thinking of doing what is practical. I appreciate the confirmation. Jan 26 at 19:04

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.