Case: a "module" (in a broad sence, i.e. something having a public interface and possibly also some private inner parts) has some complicated / involved logic inside it. Testing just the module interface will be sort of an integration testing with relation to the module's inner structure, and thus in case a error is found such testing will not localize the exact inner part / component that is responsible for the failure.
Solution: turn the complicated inner parts into modules themselves, unit-test them (and repeat these steps for them if they are too complicated themselves) and import into your original module. Now you have just a set of modules simple enough to uniit-test (both check that behavior is correct and fix errors) easily, and that's all.
Note:
there will be no need to change anything in tests of the module's (former) "sub-modules" when changing the module's contract, unless the "sub-module"'s no more offer services sufficient to fulfil the new/changed contract.
nothing will be needlessly made public i.e. the module's contract will be kept and the encapsulation maintained.
[Update]
To test some smart internal logic in cases when it is difficult to put object's inner parts (I mean members not the privately imported modules / packages) into appropriate state with just feeding it inputs via the object's public interface:
just have some testing code with friend (in C++ terms) or package (Java) access to the innards actually setting the state from inside and testing the behavior as you'd like.
this will not break the encapsulation again while providing easy direct access to the internals for testing purposes -- just run the tests as a "black box" and compile them out in release builds.