11

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?

  • 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 '14 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 '14 at 8:55
8

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.

  • 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 '14 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 – MichelHenrich Apr 1 '14 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 '14 at 14:18
  • I really would like to see a real-world example of tests that are coupling to implementation. – PositiveGuy Sep 5 '15 at 5:10
6

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.

  • Can you describe / give me a concrete example of an interface port? – PositiveGuy Sep 5 '15 at 5:12
  • what's an example of an output interface. Can you be specific in code? Same with input interface. – PositiveGuy Sep 5 '15 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 '15 at 12:02
  • An input interface is the opposite, a collaborator is both input and output. – soru Sep 5 '15 at 12:05
  • 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 '15 at 21:33

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

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