21

In his talk TDD, where did it all go wrong, Ian Cooper pushes Kent Beck's original intention behind unit testing in TDD (to test behaviours, not methods of classes specifically) and argues for avoiding coupling the tests to the implementation.

In the case of behaviour like save X to some data source in a system with a typical set of services and repositories, how can we unit test the saving of some data at the service level, through the repository, without coupling the test to implementation details (like calling a specific method)? Is avoiding this kind of coupling actually not worth the effort/bad in some way?

2
  • 1
    If you want to test that data was saved in the repository, then the test will have to actually go and check the repository to see if the data is there, right? Or am I missing something?
    – user82096
    Mar 28, 2014 at 8:53
  • My question was more about avoiding coupling the tests to an implementation detail like calling a specific method on the repository, or really if that's something that should be done.
    – Andy Hunt
    Mar 28, 2014 at 8:55

3 Answers 3

14

Your specific example is a case that you usually have to test by checking if a certain method was called, because saving X to data source means communicating with an external dependency, so the behavior you have to test is that the communication is occurring as expected.

However, this isn't a bad thing. The boundary interfaces between your application and its external dependencies are not implementation details, in fact, they are defined in the architecture of your system; which means that such a boundary is not likely to change (or if it must, it would be the least frequent kind of change). Thus, coupling your tests to a repository interface should not cause you too much trouble (if it does, consider if the interface is not stealing responsibilities from the application).

Now, consider only the business rules of an application, decoupled from UI, databases and other external services. This is were you must be free to change both structure and behavior of the code. This is where coupling tests and implementation details will force you to change more test code than production code, even when there is no change in the overall behavior of the application. This is where testing State instead of Interaction help us go faster.

PS: It's not my intention to say if testing by State or Interactions is the only true way of TDD - I believe it to be a matter of using the right tool for the right job.

4
  • When you mention "communicating with an external dependency", are you referring to external dependencies as those which are external to the unit under test, or those external to the system as a whole?
    – Andy Hunt
    Mar 31, 2014 at 7:30
  • By "external dependency" I mean anything you can be considered as a plug-in to your application. By application, I mean the business rules, agnostic of any kind of detail such as which framework to use for persistence or UI. I think Uncle Bob can explain it better, like in this talk: youtube.com/watch?v=WpkDN78P884 Apr 1, 2014 at 21:43
  • I think this is the ideal approach, like the talk says, to test on a "feature" or "behaviour" basis, and one test per feature or behaviour (or permutation of one, i.e. varying parameters). However, if I have 1 "happy" test for a feature, in order to do TDD, that means I will have a single giant commit (and code review) for that feature, which is a bad idea. How would this be avoided? Write a portion of that feature as a test and all code associated with it, then incrementally add the remainder of the feature in subsequent commits?
    – jordan
    May 9, 2014 at 14:18
  • I really would like to see a real-world example of tests that are coupling to implementation. Sep 5, 2015 at 5:10
11

My interpretation of that talk is:

  • test components, not classes.
  • test components through their interface ports.

It's not stated in the talk, but I think the assumed context for the advice is something like:

  • you are developing a system for users, not, say, a utility library or framework.
  • the goal of testing is to successfully deliver as much as possible within a competitive budget.
  • components are writen in a single, mature, probably statically typed, language like C#/Java.
  • a component is of the order of 10000-50000 lines; a Maven or VS project, OSGI plugin, etc.
  • components are written by a single developer, or closely integrated team.
  • you are following the terminology and approach of something like the hexagonal architecture
  • a component port is where you leave the local language, and its type system, behind, switching to http/SQL/XML/bytes/...
  • wrapping every port are typed interfaces, in the Java/C# sense, which can have implementations switched out to switch technologies.

So testing a component is the largest possible scope in which something can still be reasonably called unit testing. This is rather different from how some people, especially academics, use the term. It's nothing like the examples in the typical unit test tool tutorial. It does, however, match its origin in hardware testing; boards and modules are unit tested, not wires and screws. Or at least you don't build a mock Boeing to test a screw...

Extrapolating from that, and throwing in some of my own thoughts,

  • Every interface is going to be either an input, an output, or a collaborator (like a database).
  • you test the input interfaces; call the methods, assert the return values.
  • you mock the output interfaces; verify the expected methods are called for a given test case.
  • you fake the collaborators; provide a simple but working implementation

If you do that properly and cleanly, you barely need a mocking tool; it only gets used a few times per system.

A database is generally a collaborator, so it gets faked rather than mocked. This would be painful to implement by hand; luckily such things already exist.

The basic test pattern is do some sequence of operations (e.g. save and reload of a document); confirm it works. This is the same as for any other test scenario; no (working) implementation change is likely to cause such a test to fail.

The exception is where database records are written but never read by the system under test; e.g. audit logs or similar. These are outputs, and so should be mocked. The test pattern is do some sequence of operations; confirm the audit interface was called with methods and arguments as specified.

Note that even here, providing you are using a type-safe mocking tool like mockito, renaming an interface method cannot cause a test failure. If you use an IDE with the tests loaded, it will be refactored along with the method rename. If you dont, the test won't compile.

10
  • 1
    Can you describe / give me a concrete example of an interface port? Sep 5, 2015 at 5:12
  • 1
    what's an example of an output interface. Can you be specific in code? Same with input interface. Sep 5, 2015 at 6:30
  • An interface (in the Java/C# sense) wraps a port, which can be anything that talks to the outside world (d/b, socket, http, ....). An output interface is one that has no methods with return values that come from the outside world via the port, only exceptions or equivalent.
    – soru
    Sep 5, 2015 at 12:02
  • 1
    I'm not clear on what you mean by input interfaces and output interfaces. Specifically in code what is that? Are you staying for example you're sending in a repository (via injection by interface) into an interactor (business entity) and so the input interface is the repository inteface to that interactor? Sep 8, 2015 at 4:37
  • 1
    I think you are talking about an entirely different design approach and set of terminology to that described in the video. But 90% of the time a repository (i.e a database) is a collaborator, not an input or output. And so the interface to it is a collaboration interface.
    – soru
    Sep 13, 2015 at 21:33
-1

My suggestion is to use a state-based testing approach:

GIVEN We have the test DB in a known state

WHEN The service is called with arguments X

THEN Assert that the DB has changed from its original state to the expected state by calling read-only repository methods and checking their returned values

Doing that way, you don't rely on any internal algorithm of the service, and are free to refactor its implementation without having to change the tests.

The only coupling here is to the service method call and the repository calls needed to read data from the DB, which is fine.

Not the answer you're looking for? Browse other questions tagged or ask your own question.