The interpretation of “unit” in unit testing to mean a class is wrong. This and an unfortunate misunderstanding of Kent Beck’s “run unit tests in isolation” still drive developers to test their classes in extreme isolation with consequences that are irrationally attributed to test-driven development (TDD) as not being viable.
When we exaggerate decoupling in our tests, we rely on mocks and stubs to stand for collaborators that are needed by the classes being tested. As the size of our test suite grows, so do the number and usage of mocks and stubs. But, there are two major problems with mocks and stubs. First, when we use them in tests, we reveal the implementation details of the classes that depend on them. This is because we write expectations and verifications based on a very detailed knowledge of how the mocks and stubs are used. Second, unlike real objects that change organically in response to changes in their collaborations, mocks and stubs are resistant to change due to the very specific ways that they are set up for test cases.
The first issue is usually a compromise that developers accept in exchange for testability. Also, many consider it to be harmless because encapsulation is not lost and usage of the classes is unaffected.
On the other hand, the second issue is what hurts developers most and drives many to abandon TDD. The scale of the problem manifests itself once the test suite has reached a considerable size. At that point, any slight change in the nature of the collaboration between classes requires changes to numerous tests that use mocks and stubs to fake the collaboration, with countless expectations and verifications suddenly needing revision. Since the task is usually tedious, and mocks and stubs do not have actual value in production, there is little motivation to spend resources on maintaining the tests. Eventually, the practice of TDD disappears altogether.
Ideally, mocks and stubs would only be used for testing integration between components instead of collaboration between classes, but if their use cannot be avoided, their effect can at least be minimised. The most obvious way to do this is to lower the number of expectations that are set up in the test suite. By grouping tests according to the mocks and stubs that they use and the expectations that are needed, respectively, the number of test cases that need to be changed when collaborations change will be less.
There are other ways to make TDD more efficient, such as good discipline in the design of test cases (which I will write about at some point), but one improvement that can be applied immediately is to reduce the use of mocks and stubs. And the way to do that is to embrace real object collaboration within your tests.