Actually I believe there is a philosophical point to be made as well. I don’t see the purpose in directly testing a private function at all because by its very nature it is not a complete unit of observable behavior within the system, but rather an implementation detail. A private function either is a subset of one or more public functions’ implementation, or it is unreachable and therefore should be removed; this is demonstrated by the fact that the compiler can remove a private function entirely.
If all of your public functions are fully tested then all code paths through your private functions are inherently tested unless some are unreachable. Therefore any test cases you add for a private function directly is duplicated effort (and bad for programmer productivity). If a private function test case fails then either a public functions test should fail along with it resulting in redundant failures, or it means you’ve done some refactoring that broke your private function but did not change the behavior of any public interface in which case all you’re going to do is change your test case to account for your change and move on with your life.
You describe the option of testing everything through the public interface as “integration-like testing”, but I argue that you are simply defining “unit”, as each individual function in a module, to generically. I would define the “unit” in unit testing to be each function within a module that an external observer could check for observational equivalence between code changes (which is each public function). Changes to anything smaller than this cannot have any effect on the system without changing the behavior of the public function, and because of this I don’t see a private doctest any more useful than a test case that checks if your application’s database is MySQL. You can (granted assuming you are a magical wizard) migrate to Postgresql without causing any breakage in the system and yet your test will fail, and any test that can fail without a real breakage has no need to exist because it means at the end of the day you are testing an implementation detail.
I’m in the same boat and trying to talk myself out of trying to convince my startup to rewrite our Node/Python backend to Elixir, but the BEAM is just too amazing and the thought of writing anything that doesn’t run on it just makes me sad now…except maybe Rust…Rust is good too. Also sorry if my rebuttal came off harsh and/or ranty; at the end of the day I’ll always respect a desire to over test more than the opposite, and I admire your enthusiasm for useful documentation.