Coupling and Testability
Coupling and Testability
Coupling and testability go hand-in-hand. If your code is poorly coupled, it becomes hard to effectively test. Read on to get the advice of a code quality expert.
Join the DZone community and get the full member experience.Join For Free
Discover how you can take agile development to the next level with low-code.
I can learn a lot about the kind of coupling in a system by listening to how people talk about testing it. If someone says, "I couldn't test that without instantiating half the system," then I know they have some coupling issues. Unfortunately, most systems I have seen - and most systems I know other people have seen - have an enormous amount of poor coupling. That, more than anything else, makes it really difficult to test code.
Code that directly calls a service cannot be decoupled from that service. That may not be a problem when you think about normal usage, but I want our software to be able to run not just in normal usage but also in a test mode that's reflective of normal usage but more reliable and more robust.
In order to implement automated testing in a system, our task must be completely reliable. Which means the code we write that depends on some external code has to be built in such a way that when we're testing it, those external dependencies don't need to be present. Fortunately, there's a whole discipline and set of techniques around this that we call "mocking," working with test doubles, or faking implementation.
Bad coupling is one of the places we feel the pain of testability the most. When a class has multiple dependencies a developer who wants to test it has to write a test harness and mock out all the dependencies. This can be very difficult and time-consuming. But there are techniques for writing code to interface with dependencies that make it easy for us to inject test doubles as needed.
One technique for making code that's coupled to external dependencies easier to work with and to test is to separate out locating a resource from using it.
For example, if I write an API called ParseDocument(URL Webpage) that takes the webpage as input, in order to test it I need to bring up a web server that supplies that webpage, which is a lot of work just to test the parsing of the document.
So instead I could break this task into two steps: first locating the document on the web and then passing the document in to be parsed.
By separating this task out into two steps, I gain the flexibility of testing just the parsing of the document, which is the code that has my business rules. I can separate this code from the code that locates a document on a web server and returns it because it's fair to assume that code was tested by the provider of the operating system or language and it works just fine. I certainly don't feel the need to test it myself. But if I write an API that requires me to pass in a URL in order to parse the document, I've now required it to test a bunch of stuff I really don't need or want to test. In fact, the whole idea of injecting dependencies as needed instead of just newing them up and using them has tremendous potential for decoupling systems.
I'm always surprised when even the most brilliant developers I know don't really pay attention to these things when they're among the most fruitful areas for allowing us to build maintainable systems.
Published at DZone with permission of David Bernstein , DZone MVB. See the original article here.
Opinions expressed by DZone contributors are their own.