How do you test highly coupled code where data access, user interface, and business logic are interwoven in long complex methods?

This is not a “testing” issue. It’s a code issue. The code is poor, which in this case means that it probably violates one or more of the SOLID principles and Law of Demeter, does not follow many of the clean code guidelines, and exhibits many properties of “untestable code” (temporal coupling, reliance on indirect input/output etc).

What to do with such code largely depends on its life expectancy. In either case, the course of action that usually gives the most value is that of covering the critical flows with end-to-end tests. This has several benefits. First, we don’t actually need to touch the smelly code and we can create well-crafted test code. Second, the payback is immediate: we don’t need to refactor for weeks before being able to test an isolated spot somewhere in the system. Third, it actually creates a sense of safety: if we can trust the most critical functionality to remain intact, we dare to start refactoring and tidying up.

As for life expectancy, if the code is expected to go away in a relatively near future, don’t spend any time on it. Regardless of approach, refactoring poorly written legacy monoliths takes a lot of time, and requires many unit tests to be created before they can provide any value (as in ensuring testability or finding regression defects). If the functionality is needed, and can’t go away, try breaking out behavior into components or independent services that are well written and well tested. This will “strangle” the old system eventually. If you go down the path of a dedicated refactoring effort, which I don’t recommend, make sure that the refactorings are driven by actual needs. So, if you have a user story that says that the user wants to do X and Y, take a fair share of core rework into account when estimating the coding effort. While you still need to spend some time coming up with a testable target architecture, letting the “business” drive the overhaul ensures that you refactor parts of the system that are relevant to somebody. Otherwise, you run the risk of spending time refactoring the easy parts, the fun parts, or your pet parts.

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

  • Chapter 6: Drivers of Testability, pages 67-72
  • Chapter 9: Dependencies, pages 119-133
  • Chapter 18: Beyond Unit Testing, pages 258, 267-269
  • Chapter 19: Test Ideas and Heuristics, pages 271-273