TTDD - Tautological Test Driven Development (Anti Pattern)

One of the advantages of being a consultant is getting to see different environments and being able to visualise and identify patterns and anti-patterns.

"An anti-pattern is a pattern that may be commonly used but is ineffective and/or counterproductive in practice" http://en.wikipedia.org/wiki/Anti-pattern

As a first step, let’s describe TTDD – Tautological Test Driven Development as an anti-pattern of TDD (TestDrivenDevelopment). In fact TTDD is a big enemy of the combination BDD (BehaviourDrivenDesign) and TDD.

I still have the vivid memory of the day that I was pairing with Dave Coombes and he mentioned that one of the tests I had written was tautological.

Tautological: - needless, redundant repetition of an idea - repetition of same sense in different words; - the phrase "a beginner who has just started" is tautological

I did not quite understand at first why the test was tautological, i.e. needless, redundant. As a perfect leader and coach that Dave is, he helped me rewrite the test in a different way and that was when I finally understood what he had meant by that.

Example

The example below is simplified version of the one that Dave and I were using at the time. In this example, we want to test the class CarRepository that interacts with 2 collaborators as shown below:

Fullscreen capture 26052010 122651 PM

In order for CarRepository to retrieve all the cars from CarService it needs a ServiceHeader that is provided by ServiceHeaderFactory. For the purposes of this example, I’ll show the implementation of CarRepository:

public class CarRepository {
private ServiceHeaderFactory serviceHeaderFactory;
private CarService carService;

public CarRepository(ServiceHeaderFactory serviceHeaderFactory,
CarService carService) {
this.serviceHeaderFactory = serviceHeaderFactory;
this.carService = carService;
}
public Cars findAll() {
ServiceHeader serviceHeader = serviceHeaderFactory.create();
return carService.findAll(serviceHeader);
}
}

Tautological Test

 

The test below is similar to the one I wrote that we described as tautological:

@Test
public void shouldRetrieveCarsFromCarServiceUsingTheRightServiceHeader() throws Exception {
// GIVEN
ServiceHeader serviceHeader = new ServiceHeader();
ServiceHeaderFactory serviceHeaderFactoryMock = mock(ServiceHeaderFactory.class);
when(serviceHeaderFactoryMock.create()).thenReturn(serviceHeader);
CarService carServiceMock = mock(CarService.class);
CarRepository carRepository = new CarRepository(serviceHeaderFactoryMock, carServiceMock);

// WHEN
carRepository.findAll();

// THEN
verify(carServiceMock).findAll(serviceHeader);
}

Why is this test called Tautological?

 

One of the definitions of a tautology is: “repetition of same sense in different words”

If you look carefully at these 2 lines of implementation and 2 lines of test:

Test

  • when(serviceHeaderFactoryMock.create()).thenReturn(serviceHeader);
  • verify(carServiceMock).findAll(serviceHeader);

Implementation

  • ServiceHeader serviceHeader = serviceHeaderFactory.create();
  • return carService.findAll(serviceHeader);

They are almost “equivalent”. When we write tests this way, most of the time if the implementation changes, we end up changing the expectations of the test as well and yeah, the tests pass automagically. But without knowing much about its behaviour. These tests are a mirror of the implementation, therefore tautological.

 

The test below verifies the same class CarRepository, but as a black box test, i.e. it does not test the interactions, but the output of the repository. Look at the assertion.

@Test
public void shouldBeAbleToRetrieveCars() throws Exception {
// GIVEN
Cars carsFromService = new Cars(new Car("Ferrari"), new Car("Porsche"));
CarRepository carRepository = givenARepositoryAttachedToACarServiceWithCars(carsFromService);

// WHEN
Cars carsFromRepository = carRepository.findAll();

// THEN
Assert.assertEquals(carsFromService, carsFromRepository);
}

private CarRepository givenARepositoryAttachedToACarServiceWithCars(Cars carsFromService) {
ServiceHeader serviceHeader = new ServiceHeader();
ServiceHeaderFactory serviceHeaderFactoryMock = mock(ServiceHeaderFactory.class);
when(serviceHeaderFactoryMock.create()).thenReturn(serviceHeader);
CarService carServiceMock = mock(CarService.class);
when(carServiceMock.findAll(serviceHeader)).thenReturn(carsFromService);
CarRepository carRepository = new CarRepository(serviceHeaderFactoryMock, carServiceMock);
return carRepository;
}

There is still a big effort to setup the mocks and inject them into the repository. This has been extracted to the method givenARepositoryAttachedToACarServiceWithCars. However, all these mock setups make it harder to understand the intent of the test, the responsibility of the class that we are trying to test. When this happens I usually tend to use stubs instead of mocks. Here is the same version of the tests, but using Stubs:

@Test
public void shouldBeAbleToRetrieveCars() throws Exception {
// GIVEN
Cars carsFromService = new Cars(new Car("Ferrari"), new Car("Porsche"));
CarRepository carRepository = givenARepositoryAttachedToACarServiceWithCars(carsFromService);

// WHEN
Cars carsFromRepository = carRepository.findAll();

// THEN
Assert.assertEquals(carsFromService, carsFromRepository);
}

private CarRepository givenARepositoryAttachedToACarServiceWithCars(Cars carsFromService) {
ServiceHeaderFactory serviceHeaderFactory = new ServiceHeaderFactoryStub();
CarService carService = new CarServiceStub(carsFromService);
CarRepository carRepository = new CarRepository(serviceHeaderFactory, carService);
return carRepository;
}

 

download ttdd example

Download all the code from the example

It is not a rule, but I find that tautological tests have more mock setup. When we start to think about the collaborators as stubs, the tests become more behavioural.

 

What are the common characteristics of a Tautological Test?

  • Asserts more interactions with collaborators than the outputs;
  • It doesn’t really test the behaviour of the class, but only its implementation
  • The test is too white box
  • Too much mock setup deviates the intent of the test
  • Adding a new feature or changing an existing one requires changing mock expectations




Share this story