This is something so fundamental that we should all be doing it by default but sometimes, we get caught up in the day-to-day of getting something large working by creating a hierarchy instead.

In one of our older tools, we have an app that runs at the top level, this calls a class, which calls one of several classes, which calls other helper classes, etc.... you get the idea.

When code is written to be testable, the methods in each of the classes follow one of the two important rules:

1) Ideally, the method is static and/or only uses data passed into the method - we call these pure functions. They have no side-effects.
2) If the method does use fields from the class it is in, those fields should be directly injectable and/or directly testable but in this case, each method should really only access a few of these.
3) Methods should call other methods on injected objects, not on directly created objects, unless these are only in local scope.

I could write a massive long post explaining all of this although it might be better to make a video but the long-and-short of it is that if I am finding it hard to write Unit tests, not because I don't know how to write them but because I am either mocking too many objects or I cannot access certain services that are being consumed or called by the method under test, I need to re-factor my code.

You might think refactoring is dangerous but ironically, if your code is testable, then refactoring is much less risky because the tests should tell you if you break anything (and hopefully we all realise that even the best of us make mistakes, miss subtle logic etc.).

Really, the idea of TDD is that we encapsulate the abstract logic of some code clearly into a test and then make sure our methods make the tests pass. I am not an expert or even a proponent of TDD in general but various parts of it are really important for everything who codes anything serious.

A simple method that, maybe, iterates a list and renders some content with various logic is impossible to visualise in all of its branch complexity but instead, we can create a flat list of tests that are all really obvious and specific: TestEmptyList, TestDeletedResponses, TestHiddenOptions etc. (remember to include combinations of the above to make sure that if it is not rendering some things, it still renders others - but that is another story).