Progress

    0%

    Designing for Testability: The Ultimate Proof of Loose Coupling

    Learning Objectives#

    1. Read a class and predict whether it can be tested in isolation, before writing a single test.
    2. Name the seams that make a design testable, constructor injection and interfaces, and the structures that make it untestable, statics and hidden construction.
    3. Argue that testability is the measurable proof of loose coupling, and that a hard-to-test class is a design confession.

    Introduction#

    Every principle in this chapter has been a bet that a change will be cheap. Testability is the principle that makes the bet checkable. A class you can construct in a test, hand real or fake dependencies, and observe in isolation is a class that is loosely coupled by definition. A class you cannot is a class whose coupling is so tight that even the test cannot get in.

    That is the thesis, and it is stronger than it sounds. Testability is not a nice-to-have or a testing-tool concern. It is the design property that the other principles produce. When SRP, DIP, and the rest work, the class is testable as a side effect. When the class is untestable, the fix is usually not a better test, it is a better design, and the test suite is the instrument that tells you so.

    Problem Statement#

    A PaymentService is written the way a lot of production code is written.

    public class PaymentService { private static final PaymentGateway GATEWAY = new PaymentGateway(); public Receipt charge(Order order) { GATEWAY.authorize(order.getTotal()); GATEWAY.capture(); return new Receipt(order.getTotal(), "paid"); } }

    The test wants to verify the charge flow without authorizing a real card. It cannot. The gateway is a static field, constructed in the class, and no test can substitute a fake for it. The class does not accept its dependency, it welds it in. The test has two options: mock a concrete class, which the mocking libraries discourage, or skip the path and test nothing.

    The deeper problem is not the test, it is the design. The class is welded to the gateway, and the weld is exactly the coupling this chapter has been arguing against. The coupling is not abstract, it is visible: the test literally cannot get between the class and its dependency. The testability failure is the coupling made measurable. The engineer who fixes the testability, by injecting the gateway, has also fixed the coupling, and the two fixes are the same edit.

    Core Concept#

    Testability is a design property, and it has three requirements. You must be able to construct the object under test. You must be able to control its dependencies, real ones or fakes. And you must be able to observe its outputs, return values, state, or side effects on a fake. A class that meets all three is testable. A class that fails any of them is not, and the failure names the design problem.

    Construction is the first seam. The class's dependencies should come through the constructor, because the constructor is the test's entry point. new PaymentService(gateway) is testable, the test passes a fake gateway. new PaymentService() that internally does new PaymentGateway() is not, the test has no way in. This is the same rule as the dependency inversion article: the constructor receives dependencies, and the class does not build the things it needs. The testability requirement is that rule from the test's point of view.

    Control is the second seam, and it is about interfaces. The test needs to substitute a fake for the real dependency, and substitution requires the type of the dependency to be an interface the fake can implement. PaymentGateway as an interface, with a StripePaymentGateway for production and a FakePaymentGateway for tests, gives the test control. A concrete class with final methods does not, and the test is stuck with the real thing. The interface is the seam that the interface segregation and dependency inversion articles demanded, and the test is its consumer.

    Observation is the third requirement, and it is the one beginners forget. The test must be able to see what the class did. If the class writes to a static logger, a global cache, or a real database, the test cannot observe the result. If it calls a gateway interface, the fake records the calls, and the test asserts on the record. The observable seam is the interface boundary, and a design that routes its effects through injectable collaborators is observable by construction.

    The absence of the seams is the negative definition, and the tells are specific. A static call that the class makes directly, Database.query(...), a static field like the gateway, a singleton the class fetches, Config.getInstance(), a dependency constructed inside the method. Each one is a place where the test cannot substitute, and each one is a coupling to something the class does not control. The tells are the chapter's principles read backwards: a class that cannot be constructed with its dependencies is violating dependency inversion, a class that cannot be observed is violating separation of concerns.

    The strongest version of the argument is the reverse. A class you can test in isolation is a class whose coupling you have already fixed, even if you did not do it deliberately. The seams that let the test in, the constructor, the interface, the fake, are the same seams that let a change in. The fake gateway that substitutes for the real one in a test is the same seam that substitutes a new gateway in production. Testability is not a testing feature, it is the proof that the seams exist.

    The instruments that test on top of these seams are secondary to the seams themselves. A unit test constructs the class, hands it fakes, and asserts on the observable result. A mock verifies the calls the class made on its collaborators. The testing framework does not create the testability, the design does, and the framework is only as good as the seams it is given. A class welded to its gateway is not made testable by any framework, because no framework can substitute for a dependency the class built itself.

    There is one honest caveat, and it keeps the thesis from overshooting. Testability is the proof of loose coupling at the unit level, and unit-level testability is not the same as system-level correctness. A fully mocked test can pass while the wiring, the real gateway, the real database, the real configuration, fails. The unit test proves the seams exist and the logic is right. The integration test proves the wiring is right. The design argument is about the seams, and the seams are proven by the unit test. The integration test is a different instrument for a different question.

    Real Production Usage#

    Spring's constructor injection is the production machinery that makes the thesis run. A service that declares its dependencies in its constructor can be constructed in a test with fakes, new OrderService(orderRepository, pricingRule), and it can be constructed by the container with real beans. The same seam serves both. The Spring team's insistence on constructor injection is not a style preference, it is the testability requirement stated as a framework convention.

    The repository mock is the everyday proof. A service that depends on OrderRepository as an interface gets a fake repository in its unit test, an in-memory implementation or a mock, and the test verifies the service's logic without a database. The fake is possible because the interface exists, and the interface exists because the coupling was inverted. The test is not a workaround, it is the consumer of the seam.

    The class that needs PowerMock or Mockito's static mocking is the counterexample, and the industry has a name for it: a class so tightly coupled that the mocking library has to be contorted to test it. The static mock is not a solution, it is a confession. The engineer who reaches for it should be reading the message the class is sending, the dependency should be injectable, and the fix is the design, not the library.

    Common Mistakes#

    The most common mistake is treating the testing framework as the testability. A team that writes tests through a Spring Boot test context for everything, spinning up the whole application to test one class, has moved the seams to the framework level and lost the unit test. The class is still welded, the framework is just doing the welding around it. The unit test that constructs the class with a fake is the testability proof, and the full-context test is the expensive substitute.

    The second mistake is mocking concrete classes to patch the missing seam. The gateway is a concrete class, the mock substitutes it anyway, and the team declares victory. The mock works, and the coupling is still there, the class still cannot be constructed with a real alternative, and the next change still ripples. The mock that mocks a concrete class is the test version of the facade from the cohesion article, moving the problem without fixing it.

    The third mistake is assuming integration tests cover for unit design. A system test that exercises the real gateway, database, and cache can pass while the design is tightly coupled, because the test bypasses the seams instead of using them. The passing integration test gives the team permission to ignore the class-level weld, and the weld is exactly what makes the next change expensive. The unit test is the instrument that sees the weld, and skipping it is skipping the measurement.

    Interview Perspective#

    The question "how would you make this class testable" is the design interview in miniature. The weak answer starts talking about Mockito. The strong answer starts with the seams. "I would pass the dependency through the constructor as an interface, so the test can construct the class with a fake. The class should not build its own gateway. Once it accepts the gateway, the test controls it, and the coupling is gone, the same seam that lets a test fake it lets production swap it."

    The follow-up "is the ability to mock proof of good design" wants the honest line. "Mocking a concrete class is not proof, it is a patch. Mocking through an interface is proof the seam exists. The class you can construct with a fake and observe is loosely coupled by definition, and the class that needs static mocking is telling you the coupling is still welded in."

    The sharper question: "what does testability actually prove." The strong answer states the thesis. "It proves the seams exist. The constructor, the interface, the injectable collaborator, those are the same seams that make a change cheap. A class you can test in isolation is a class whose coupling you have already fixed, whether you meant to or not."

    Knowledge Check#

    A ReportService reads from a static Database and writes to a static AuditLog. Name the three requirements of testability and state which one each static breaks.

    A class takes a concrete EmailClient in its constructor, and a mock substitutes it in tests. Why is the test not proof of loose coupling, and what change to the type would make it proof?

    A unit test for a service passes an in-memory fake repository, and an integration test exercises the real database. State what each test proves about the design, and which one proves the seams exist.

    Key Takeaways#

    • Testability is construction, control, and observation, and a class that fails any of them names its own design problem.
    • The constructor and the interface are the seams that let a test in, and the same seams let a change in.
    • Mocking a concrete class or using static mocking is a patch, and the fix is the design, not the library.
    • A class you can test in isolation is a class whose coupling is already fixed, which is the measurable proof of everything else in this chapter.

    What's Next#

    This closes the chapter on the principles. Every principle here has been about deciding how the code should be shaped, and the next chapter changes the medium entirely. UML and Design Visualization is about the drawings that let you communicate a design before it is code, the class diagram, the sequence diagram, the state machine. The shift is from arguing about seams in prose to drawing them, and from design as a private judgment to design as a shared picture that a team can review. The principles you just learned, the coupling, the contracts, the boundaries, are exactly what the diagrams will be asked to show.

    Want to Master Spring Boot and Land Your Dream Job?

    Struggling with coding interviews? Learn Data Structures & Algorithms (DSA) with our expert-led course. Build strong problem-solving skills, write optimized code, and crack top tech interviews with ease

    Learn more

    Last updated on Aug 18, 2026

    Was it helpful?

    Subscribe to our newsletter

    Read articles from Coding Shuttle directly inside your inbox. Subscribe to the newsletter, and don't miss out.