Question: 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.

Answer: 
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.*
> 
> 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](https://en.wikipedia.org/wiki/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](https://en.wikipedia.org/wiki/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.