Try to only write tests for public/exported methods. Don't test internal functions (there will be exceptions as with anything but this should be a general rule).<p>Design your interfaces and method signatures in a way which would minimize future need for changes.<p>For example, do not pass configuration options to your module as option1 and option2, instead wrap them in options/config object and pass that to your module.<p>This allows you to add more configuration options in the future without having to break any existing interface.<p>Finally, when it comes to brittle unit tests, an extra layer of functional / integration tests is what you should have.<p>These tests will be less brittle as they will break in more cases (i.e. if there is an issue on the edges of "units" of your codebase).<p>The tradeoff will be that functional/integration tests will be slower and more difficult to execute (I suggest using docker-compose to run integration tests in a platform agnostic way).
You want to write the tests at the boundary where you want stability, where you want to prevent change.<p>Sometimes you don't want to prevent change because it's an implementation detail. That can mean private APIs, but also whole layers of abstraction.<p>Common layering, for example, is:<p>1. Low level operation that needs to be stable, so needs tests.<p>2. Medium level abstraction layer, not core business logic and builds on stable layer.<p>3. High level abstraction, the exposed public business logic API. Public API so needs tests.<p>Medium layer does not necessarily need tests in this case.<p>I have a bit higher level discussion of this in a talk I gave in May (first video): <a href="https://codewithoutrules.com/talks/" rel="nofollow">https://codewithoutrules.com/talks/</a>