Sadly, most posts/introductions about unit tests are usually way too simple with easy pure functions that just calculate some number. You don't have this kind of logic in real-world applications.
I have to deal with way more complicated scenarios daily involving object graphs that require initialization, external dependencies, drag and drop, framework objects that you can't mock and so on. I've yet to read a tutorial (or even book) that covers these cases.
How do I deal with legacy code that is not necessarily bad, but doesn't use DI and is not as easily testable as it ideally should be? How do I test public methods that don't return a value, but change something deeper down the call stack (internally)? I'd really like to read an article about how to tackle these problems!
Great questions! I actually have a whole lot to say on this but that'd have to be a whole new article.
For greenfield, functional programming is the answer. You don't need to test "changes deeper down the call stack" because there's no such thing as a change. Pass a value, get a value back.
We've written large complex applications with two flavours of code:
Both flavours test-driven, not test-after.
You say that this legacy code is not necessarily bad but missing testability. Assuming this, I say the following:
The cost of fixing is still less than rewriting from scratch. See Joel Spolsky's article about "Things you should never do".
Pick up a copy of Working Effectively with Legacy Code by Michael Feathers. It's an entire book about getting code that doesn't have tests into a harness. Essentially, how to deal with what you're talking about. It's worth every penny.
We're a place where coders share, stay up-to-date and grow their careers.
We strive for transparency and don't collect excess data.