How can we build a testable system and ensure that it stays testable as it evolves?

First, understand the drivers of testability: controllability, observability, and smallness, as well as indirect input/output and temporal coupling. This will provide a foundation for both the overall architecture and the day to day coding work.

Then, write tests. Lots of them, of all kinds. This will ensure that your system stays testable and it will teach you how it must be designed to accommodate all the tests. TDD will give you all of this out of the box, but it’s not mandatory, As long as all code you write has various kinds of tests, it’ll stay testable.

The best way to learn about testability is actually to attempt to make a legacy system testable. You’d learn all the bad constructs in practice and what not to do.

Book References
Read more about these topics in Developer Testing: Building Quality into Software:

  • Chapter 4: Testability from a Developer’s Perspective, pages 37-55
  • Chapter 6: Drivers of Testability, pages 67-78
  • Chapter 9: Dependencies, pages 119-133