Tangles in Test Code: Hidden Arrange/Given/Setup

Michael Kutz
5 min readMar 26, 2024
Some code with hidden arrange and some symbols in foreground.

There is a certain sloppiness in the industry when writing test code. This causes what I call tangles. A rather subtle one of these is the use of implicit arrange/given/setup steps that aren’t directly visible from the test code.

Why is this a Tangle?

Hiding technical setup steps in test code can be great as it reduces the complexity of the test code and keeps the focus of the reader to the things important for the test. However, this becomes a tangle as soon as the test code explicitly refers to what happens in the hidden arrange.

For example the following test code is meant to test if it is possible to GET a list of all unicorns via the service’s HTTP API.

@SpringBootTest(webEnvironment = WebEnvironment.RANDOM_PORT, classes = LocalTestApplication.class)
class UnicornApiTest {

@Value("http://localhost:${local.server.port}")
String baseUrl;

@Autowired TestRestTemplate restTemplate;
ObjectMapper objectMapper = new ObjectMapper();

@Test
void get_all_unicorns() throws JsonProcessingException {
var response = restTemplate.getForEntity("%s/unicorns".formatted(baseUrl), String.class);

assertThat(response.getStatusCode()).isEqualTo(HttpStatus.OK);

var body = objectMapper.readTree(response.getBody());

assertThat(body).isNotNull();
assertThat(body.isArray()).isTrue();
assertThat(body.size()).isEqualTo(1); // WHY?!
}

// …

The problematic part is in the final assert. It expects the response body to contain exactly one item. Why? There’s no setup method being called, no super class to the test, no interfaces with default implementations, …. Well, it’s just a number, I guess.

But wait, there’s another test case

  // …

@Test
void get_single_unicorn() throws JsonProcessingException {
var response =
restTemplate.getForEntity(
"%s/unicorns/%s".formatted(baseUrl, "44eb6bdc-a0c9-4ce4-b28b-86d5950bcd23"),
String.class);
var body = objectMapper.readTree(response.getBody());

assertThat(body.get("id").asText())
.isEqualTo("44eb6bdc-a0c9-4ce4-b28b-86d5950bcd23");
assertThat(body.get("name").asText())
.isEqualTo("Grace");
assertThat(body.get("maneColor").asText())
.isEqualTo("RAINBOW");
assertThat(body.get("hornLength").asInt())
.isEqualTo(42);
assertThat(body.get("hornDiameter").asInt())
.isEqualTo(10);
assertThat(body.get("dateOfBirth").asText())
.isEqualTo("1982-02-19");
}
}

The test effectively verifies the response’s body structure when GETing a single unicorn from the API. But there’s still no hint where the verified data is defined.

The mechanism used here is a feature of Spring Boot: Initialize a Database Using Basic SQL Scripts. The mere existence of an SQL script at src/test/resources/data.sql makes Spring Boot execute this script at start up.

TRUNCATE TABLE
unicorns;

INSERT
INTO unicorns(
id,
name,
mane_color,
horn_length,
horn_diameter,
date_of_birth
)
VALUES(
'44eb6bdc-a0c9-4ce4-b28b-86d5950bcd23',
'Grace',
'RAINBOW',
42,
10,
'1982-2-19'
);

Now, this may seem great at first. This script initializes our database in a way that all reading API endpoints return something to verify.

But, what if we require another example unicorn to test an edge case? We might just add another INSERT statement to the file, right? Well, that would break the first test above as there will be then two items in the returned list. So there are non-obvious reasons for tests to fail!

Another reason why I consider this a tangle is that the tests functionality is somewhat obscured. Does the API come with a pre-installed dataset in the production code? If we wanted to test the actual initial state of the application, we’d need to actively truncate the database. If we have a test case for deletion of data, we’d only have one shot.

How to Untangle?

Obviously we should make the hidden arrange steps visible in the test case. This can be done in different ways.

In order to gain the flexibility we need to create different scenarios, I’d suggest creating a utility class that allows us to put the application under test into different desired states. E.g. no unicorns in the database, one unicorn with a known ID that can than be updated or deleted, many unicorns to test pagination…

My personal favourite solution here is to use a Test Data Manager, which enables us to put data objects into the database. This class would be a @Component and so can @Autowire things from the ApplicationContext like either the DataSource to create a JdbcTemplate:

@Component
public class TestDataManager {

private final JdbcTemplate jdbcTemplate;

public TestDataManager(DataSource dataSource) {
jdbcTemplate = new JdbcTemplate(dataSource);
}

public TestDataManager withUnicorn(Unicorn unicorn) {
jdbcTemplate.update(
"""
INSERT
INTO
unicorns(
id,
name,
mane_color,
horn_length,
horn_diameter,
date_of_birth
)
VALUES(?, ?, ?, ?, ?, ?);
""",
unicorn.id(),
unicorn.name(),
unicorn.maneColor().name(),
unicorn.hornLength(),
unicorn.hornDiameter(),
unicorn.dateOfBirth());
return this;
}

public TestDataManager clear() {
jdbcTemplate.execute("TRUNCATE TABLE unicorns;");
return this;
}
}

This allows us to put the SQL statements from the data.sql in a better place. Note that we can also use this pattern to verify states. E.g. after POSTing a Unicorn, we can use a simple SELECT statement to verify it found its way into persistence.

Alternatively, we can inject the UnicornRepository and use the already implemented statements there:

@Component
public class TestDataManager {

private final UnicornRepository unicornRepository;

public TestDataManager(UnicornRepository unicornRepository) {
this.unicornRepository = unicornRepository;
}

public TestDataManager withUnicorn(Unicorn unicorn) {
unicornRepository.save(unicorn);
return this;
}

public TestDataManager clear() {
unicornRepository.clear();
return this;
}
}

This implementation clearly has less code, but the test cases will depend on the internal implementation. The clear method was only added to support test cases and is not motivated by production requirements. In my opinion this is a debatable trade off.

I recommend to combine this with a proper TestDataBuilder to create the required data objects and avoid unnecessary Long Arrange code blocks:

public class UnicornTestDataBuilder {

private final SecureRandom random = new SecureRandom();
private UUID id = randomUUID();
private String name = randomAlphabetic(8);
private ManeColor maneColor = ManeColor.values()[random.nextInt(ManeColor.values().length)];
private Integer hornLength = random.nextInt(1, 101);
private Integer hornDiameter = random.nextInt(1, 41);
private LocalDate dateOfBirth =
LocalDate.now()
.minusDays(random.nextInt(0, 31))
.minusMonths(random.nextInt(0, 13))
.minusYears(random.nextInt(0, 101));

private UnicornTestDataBuilder() {}
public static UnicornTestDataBuilder aUnicorn() {
return new UnicornTestDataBuilder();
}

public UnicornTestDataBuilder dateOfBirth(LocalDate dateOfBirth) {
this.dateOfBirth = dateOfBirth;
return this;
}

// … more manipulators

public Unicorn build() {
return new Unicorn(id, name, maneColor, hornLength, hornDiameter, dateOfBirth);
}
}

Putting it all together, we end up with the following test cases

@SpringBootTest(webEnvironment = WebEnvironment.RANDOM_PORT, classes = LocalTestApplication.class)
class UnicornApiTest {

@Value("http://localhost:${local.server.port}")
String baseUrl;

@Autowired TestRestTemplate restTemplate;
ObjectMapper objectMapper = new ObjectMapper();

@Autowired TestDataManager testDataManager

@Test
void get_all_unicorns() throws JsonProcessingException {
testDataManager.clear().withUnicorn(aUnicorn.build());

var response = restTemplate.getForEntity("%s/unicorns".formatted(baseUrl), String.class);

assertThat(response.getStatusCode()).isEqualTo(HttpStatus.OK);

var body = objectMapper.readTree(response.getBody());

assertThat(body).isNotNull();
assertThat(body.isArray()).isTrue();
assertThat(body.size()).isEqualTo(1); // WHY?!
}

@Test
void get_single_unicorn() throws JsonProcessingException {
var unicorn = aUnicorn.build();
testDataManager.clear().withUnicorn(unicorn);

var response =
restTemplate.getForEntity(
"%s/unicorns/%s".formatted(baseUrl, unicorn.id()),
String.class);
var body = objectMapper.readTree(response.getBody());

assertThat(body.get("id").asText())
.isEqualTo(unicorn.id());
assertThat(body.get("name").asText())
.isEqualTo(unicorn.name());
assertThat(body.get("maneColor").asText())
.isEqualTo(unicorn.maneColor().name());
assertThat(body.get("hornLength").asInt())
.isEqualTo(unicorn.hornLength());
assertThat(body.get("hornDiameter").asInt())
.isEqualTo(unicorn.hornDiameter());
assertThat(body.get("dateOfBirth").asText())
.isEqualTo(unicorn.dateOfBirth.format(DateTimeFormatter.ofPattern("yyyy-MM-dd")));
}
}

Conclusion

Using a Test Data Manager (or similar concepts) allows to put the unit under test into a required state quickly and effectively, while it is clearly visible in the test code.

It allows referring to the data used in the arrange phase to avoid Magic Values in the assert block (see second test case above), and enables testing more complex edgy cases without changing the test setup for all cases.

Certainly this makes the test cases less black boxy. After all we use at least the applications data source or even the very internal repository API. I consider this a very good trade off given the above advantages.

The code blocks in this article are partly simplified versions of example in the Untangle Your Spaghetti Test Code workshop, I created with Christian Baumann. You can find the full code and a lot more tangles on GitHub.

--

--

Michael Kutz

I've been a software engineer since 2009, worked in various agile projects & got a taste for quality assurance. Today I'm a quality engineer at REWE digital.