Michael Kutz
Quality Engineer at REWE digital, Conference Speaker about QA & Agile, Founder of Agile QA Cologne meetup, Freelance QA Consultant
Test code is code.
What's considered bad in production code, is bad in test code.
Its intention should be immediately obvious.
Its functionality shouldn't be obscured.
Its results should be meaningful.
Its failure causes should be easy to find.
Its maintenance shouldn't be hard.
Tests rely on non-obvious setup mechanisms
@SpringBootTest(webEnvironment = WebEnvironment.RANDOM_PORT, classes = LocalTestApplication.class)
class ApplicationTest {
@Value("http://localhost:${local.server.port}")
String baseUrl;
@Autowired TestRestTemplate restTemplate;
ObjectMapper objectMapper = new ObjectMapper();
@Test
void getUnicornsWorksAndReturnsNonEmptyList() 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);
}
}
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'
);
/src/test/resources/data.sql
Test code is code.
What's considered bad in production code, is bad in test code.
Its intention should be immediately obvious.
Its functionality shouldn't be obscured.
Its results should be meaningful.
Its failure causes should be easy to find.
Its maintenance shouldn't be hard.
Literal values in the test code chosen for no immediately obvious reason
class UnicornTest {
@Test
void ageWorksHereAlso() {
var gilly =
new Unicorn(
randomUUID(),
"Gilly",
ManeColor.RED,
111,
11,
LocalDate.now().minusYears(62).plusDays(1));
assertThat(gilly.age()).isEqualTo(61);
}
}
Test code is code.
What's considered bad in production code, is bad in test code.
Its intention should be immediately obvious.
Its functionality shouldn't be obscured.
Its results should be meaningful.
Its failure causes should be easy to find.
Its maintenance shouldn't be hard.
Data objects are being created with all fields
Even if the set value doesn't matter
@SpringBootTest(webEnvironment = WebEnvironment.RANDOM_PORT, classes = LocalTestApplication.class)
class ApplicationTest {
@Value("http://localhost:${local.server.port}")
String baseUrl;
@Autowired TestRestTemplate restTemplate;
ObjectMapper objectMapper = new ObjectMapper();
@Test
@DirtiesContext
void postNewUnicorn() {
var garryJson =
objectMapper.writeValueAsString(
Map.of(
"name", "Garry",
"maneColor", "BLUE",
"hornLength", 37,
"hornDiameter", 11,
"dateOfBirth", "1999-10-12"));
var response =
restTemplate.exchange(
post("%s/unicorns/".formatted(baseUrl))
.header("Content-Type", "application/json")
.body(garryJson),
String.class);
assertThat(response.getStatusCode()).isEqualTo(HttpStatusCode.valueOf(201));
assertThat(response.getHeaders().get("Location")).isNotNull().hasSize(1);
var anotherResponse =
restTemplate.getForEntity(
requireNonNull(response.getHeaders().get("Location")).getFirst(), String.class);
assertThat(anotherResponse.getStatusCode()).isEqualTo(HttpStatusCode.valueOf(200));
}
}
Test code is code.
What's considered bad in production code, is bad in test code.
Its intention should be immediately obvious.
Its functionality shouldn't be obscured.
Its results should be meaningful.
Its failure causes should be easy to find.
Its maintenance shouldn't be hard.
Arrangement or Assertion code is duplicated in multiple test cases and classes
@SpringBootTest(webEnvironment = WebEnvironment.RANDOM_PORT, classes = LocalTestApplication.class)
class ApplicationTest {
@Value("http://localhost:${local.server.port}")
String baseUrl;
@Autowired TestRestTemplate restTemplate;
ObjectMapper objectMapper = new ObjectMapper();
@Test
@DirtiesContext
void postNewUnicorn() {
var garryJson = objectMapper.writeValueAsString(
Map.of(
"name", "Garry",
"maneColor", "BLUE",
"hornLength", 37,
"hornDiameter", 11,
"dateOfBirth", "1999-10-12"));
var response =
restTemplate.exchange(
post("%s/unicorns/".formatted(baseUrl))
.header("Content-Type", "application/json")
.body(garryJson),
String.class);
assertThat(response.getStatusCode()).isEqualTo(HttpStatusCode.valueOf(201));
assertThat(response.getHeaders().get("Location")).isNotNull().hasSize(1);
var anotherResponse =
restTemplate.getForEntity(
requireNonNull(response.getHeaders().get("Location")).getFirst(), String.class);
assertThat(anotherResponse.getStatusCode()).isEqualTo(HttpStatusCode.valueOf(200));
}
@Test
@DirtiesContext
void testHLZero() throws JsonProcessingException {
var larryJson =
objectMapper.writeValueAsString(
Map.of(
"name", "Larry",
"maneColor", "BLUE",
"hornLength", 0,
"hornDiameter", 18,
"dateOfBirth", "1999-10-12"));
var response =
restTemplate.exchange(
post("%s/unicorns/".formatted(baseUrl))
.header("Content-Type", "application/json")
.body(larryJson),
List.class);
assertThat(response.getStatusCode()).isEqualTo(HttpStatusCode.valueOf(400));
assertThat(response.getHeaders().containsKey("Location")).isFalse();
assertThat(response.getBody()).contains("hornLength must be between 1 and 100");
}
@Test
@DirtiesContext
void testHLTooMuch() throws JsonProcessingException {
var larryJson =
objectMapper.writeValueAsString(
Map.of(
"name", "Larry",
"maneColor", "BLUE",
"hornLength", 101,
"hornDiameter", 18,
"dateOfBirth", "1999-10-12"));
var response =
restTemplate.exchange(
post("%s/unicorns/".formatted(baseUrl))
.header("Content-Type", "application/json")
.body(larryJson),
List.class);
assertThat(response.getStatusCode()).isEqualTo(HttpStatusCode.valueOf(400));
assertThat(response.getHeaders().containsKey("Location")).isFalse();
assertThat(response.getBody()).contains("hornLength must be between 1 and 100");
}
@Test
@DirtiesContext
void testHDNotGiven() throws JsonProcessingException {
var larryJson =
objectMapper.writeValueAsString(
Map.of(
"name", "Larry",
"maneColor", "BLUE",
"hornLength", 66,
"hornDiameter", 0,
"dateOfBirth", "1999-10-12"));
var response =
restTemplate.exchange(
post("%s/unicorns/".formatted(baseUrl))
.header("Content-Type", "application/json")
.body(larryJson),
List.class);
assertThat(response.getStatusCode()).isEqualTo(HttpStatusCode.valueOf(400));
assertThat(response.getHeaders().containsKey("Location")).isFalse();
assertThat(response.getBody()).contains("hornDiameter must be between 1 and 40");
}
}
Test code is code.
What's considered bad in production code, is bad in test code.
Its intention should be immediately obvious.
Its functionality shouldn't be obscured.
Its results should be meaningful.
Its failure causes should be easy to find.
Its maintenance shouldn't be hard.
There are multiple acts in one test case
The act is bloated with technical details
@SpringBootTest(webEnvironment = WebEnvironment.RANDOM_PORT, classes = LocalTestApplication.class)
class ApplicationTest {
@Value("http://localhost:${local.server.port}")
String baseUrl;
@Autowired TestRestTemplate restTemplate;
ObjectMapper objectMapper = new ObjectMapper();
@Test
@DirtiesContext
void postNewUnicorn() {
var garryJson = objectMapper.writeValueAsString(
Map.of(
"name", "Garry",
"maneColor", "BLUE",
"hornLength", 37,
"hornDiameter", 11,
"dateOfBirth", "1999-10-12"));
var response =
restTemplate.exchange(
post("%s/unicorns/".formatted(baseUrl))
.header("Content-Type", "application/json")
.body(garryJson),
String.class);
assertThat(response.getStatusCode()).isEqualTo(HttpStatusCode.valueOf(201));
assertThat(response.getHeaders().get("Location")).isNotNull().hasSize(1);
var anotherResponse =
restTemplate.getForEntity(
requireNonNull(response.getHeaders().get("Location")).getFirst(), String.class);
assertThat(anotherResponse.getStatusCode()).isEqualTo(HttpStatusCode.valueOf(200));
}
}
Test code is code.
What's considered bad in production code, is bad in test code.
Its intention should be immediately obvious.
Its functionality shouldn't be obscured.
Its results should be meaningful.
Its failure causes should be easy to find.
Its maintenance shouldn't be hard.
Verify multiple aspects in one test case
Check properties of data objects one by one
@SpringBootTest(webEnvironment = WebEnvironment.RANDOM_PORT, classes = LocalTestApplication.class)
class ApplicationTest {
@Value("http://localhost:${local.server.port}")
String baseUrl;
@Autowired TestRestTemplate restTemplate;
ObjectMapper objectMapper = new ObjectMapper();
@Test
void getSingleUnicornWorksAndReturnsData() throws JsonProcessingException {
restTemplate.getForEntity(
"%s/unicorns/%s".formatted(baseUrl, "44eb6bdc-a0c9-4ce4-b28b-86d5950bcd23"),
String.class);
var unicornData = objectMapper.readTree(response.getBody());
assertThat(unicornData.has("name")).isTrue();
assertThat(unicornData.get("name").asText()).isEqualTo("Grace");
assertThat(unicornData.has("maneColor")).isTrue();
assertThat(unicornData.get("maneColor").asText()).isEqualTo("RAINBOW");
assertThat(unicornData.has("hornLength")).isTrue();
assertThat(unicornData.get("hornLength").asInt()).isEqualTo(42);
assertThat(unicornData.has("hornDiameter")).isTrue();
assertThat(unicornData.get("hornDiameter").asInt()).isEqualTo(10);
assertThat(unicornData.has("dateOfBirth")).isTrue();
assertThat(unicornData.get("dateOfBirth").asText()).isEqualTo("1982-02-19");
}
}
Test code is code.
What's considered bad in production code, is bad in test code.
Its intention should be immediately obvious.
Its functionality shouldn't be obscured.
Its results should be meaningful.
Its failure causes should be easy to find.
Its maintenance shouldn't be hard.
Multiple interactions with the unit under test in the same test case
@SpringBootTest(webEnvironment = WebEnvironment.RANDOM_PORT, classes = LocalTestApplication.class)
class ApplicationTest {
@Value("http://localhost:${local.server.port}")
String baseUrl;
@Autowired TestRestTemplate restTemplate;
ObjectMapper objectMapper = new ObjectMapper();
@Test
@DirtiesContext
void postNewUnicorn() {
var garryJson =
"{\"dateOfBirth\":\"1999-10-12\",\"hornDiameter\":11,\"hornLength\":37,\"maneColor\":\"BLUE\",\"name\":\"Garry\"}";
var response =
restTemplate.exchange(
post("%s/unicorns/".formatted(baseUrl))
.header("Content-Type", "application/json")
.body(garryJson),
String.class);
assertThat(response.getStatusCode()).isEqualTo(HttpStatusCode.valueOf(201));
assertThat(response.getHeaders().get("Location")).isNotNull().hasSize(1);
var anotherResponse =
restTemplate.getForEntity(
requireNonNull(response.getHeaders().get("Location")).getFirst(), String.class);
assertThat(anotherResponse.getStatusCode()).isEqualTo(HttpStatusCode.valueOf(200));
}
}
Test code is code.
What's considered bad in production code, is bad in test code.
Its intention should be immediately obvious.
Its functionality shouldn't be obscured.
Its results should be meaningful.
Its failure causes should be easy to find.
Its maintenance shouldn't be hard.
Test case names don't reflect the actual test content
Names are chosen inconsistently
@SpringBootTest(webEnvironment = WebEnvironment.RANDOM_PORT, classes = LocalTestApplication.class)
class ApplicationTest {
@Value("http://localhost:${local.server.port}")
String baseUrl;
@Autowired TestRestTemplate restTemplate;
ObjectMapper objectMapper = new ObjectMapper();
@Test
void postInvalidUnicornYieldsA500Response() {
var response = restTemplate
.postForEntity(url, String.class);
assertThat(response.getStatusCode())
.isEqualTo(HttpStatusCode.valueOf(400));
assertThat(response
.getHeaders()
.containsKey("Location"))
.isFalse();
assertThat(response.getBody())
.contains("invalid unicorn");
}
}
Test code is code.
What's considered bad in production code, is bad in test code.
Its intention should be immediately obvious.
Its functionality shouldn't be obscured.
Its results should be meaningful.
Its failure causes should be easy to find.
Its maintenance shouldn't be hard.
Massive use of Mocks can lead to a very brittle test suite
Tests break even for trivial refactoring due to a behaviour over-specification
Test code is code.
What's considered bad in production code, is bad in test code.
Its intention should be immediately obvious.
Its functionality shouldn't be obscured.
Its results should be meaningful.
Its failure causes should be easy to find.
Its maintenance shouldn't be hard.
All tests have three parts (or less):
Apply a consistent test case naming scheme
class <ClassUnderTest>Test {
void <methodUnderTest>_<stateUnderTest>() {
… // see code for <expectedBehavior>
}
}
class UnicornTest {
@Test
void ageWorks() {}
@Test
void ageWorksHereToo() {}
@Test
void ageWorksHereAlso() {}
@Test
void negativeAge() {}
}
class <ClassUnderTest>Test {
void <methodUnderTest>_<stateUnderTest>() {
… // see code for <expectedBehavior>
}
}
class UnicornTest {
@Test
void age_birthday_whole_years_ago() {}
@Test
void ageWorksHereToo() {}
@Test
void ageWorksHereAlso() {}
@Test
void negativeAge() {}
}
class <ClassUnderTest>Test {
void <methodUnderTest>_<stateUnderTest>() {
… // see code for <expectedBehavior>
}
}
class UnicornTest {
@Test
void age_birthday_whole_years_ago() {}
@Test
void age_birthday_years_plus_ago() {}
@Test
void ageWorksHereAlso() {}
@Test
void negativeAge() {}
}
class <ClassUnderTest>Test {
void <methodUnderTest>_<stateUnderTest>() {
… // see code for <expectedBehavior>
}
}
class UnicornTest {
@Test
void age_birthday_whole_years_ago() {}
@Test
void age_birthday_years_plus_ago() {}
@Test
void age_birthday_years_minus_ago() {}
@Test
void negativeAge() {}
}
class <ClassUnderTest>Test {
void <methodUnderTest>_<stateUnderTest>() {
… // see code for <expectedBehavior>
}
}
class UnicornTest {
@Test
void age_birthday_whole_years_ago() {}
@Test
void age_birthday_years_plus_ago() {}
@Test
void age_birthday_years_minus_ago() {}
@Test
void age_birthday_in_future() {}
}
Test code is code.
What's considered bad in production code, is bad in test code.
Its intention should be immediately obvious.
Its functionality shouldn't be obscured.
Its results should be meaningful.
Its failure causes should be easy to find.
Its maintenance shouldn't be hard.
Split test cases with multiple acts with assumtions
@Test
@DirtiesContext
void postNewUnicorn() {
var garryJson = /* … */;
var response = restTemplate.exchange(url, garryJson, /* … */);
assertThat(response.getStatusCode()).isEqualTo(HttpStatusCode.valueOf(201));
assertThat(response.getHeaders().get("Location")).isNotNull().hasSize(1);
var location = response.getHeaders().get("Location").getFirst()
var anotherResponse = restTemplate.getForEntity(location, String.class);
assertThat(anotherResponse.getStatusCode()).isEqualTo(HttpStatusCode.valueOf(200));
}
@Test
@DirtiesContext
void postNewUnicorn() {
var garryJson = /* … */;
var response = restTemplate.exchange(url, garryJson, /* … */);
assertThat(response.getStatusCode()).isEqualTo(HttpStatusCode.valueOf(201));
assertThat(response.getHeaders().get("Location")).isNotNull().hasSize(1);
}
@Test
@DirtiesContext
void getLocationHeader() {
var garryJson = /* … */;
var postResponse = restTemplate.exchange(url, garryJson, /* … */);
assumeThat(postResponse.getStatusCode()).isEqualTo(HttpStatusCode.valueOf(201));
var location = response.getHeaders().get("Location").getFirst()
var response = restTemplate.getForEntity(location, String.class);
assertThat(anotherResponse.getStatusCode()).isEqualTo(HttpStatusCode.valueOf(200));
}
Test code is code.
What's considered bad in production code, is bad in test code.
Its intention should be immediately obvious.
Its functionality shouldn't be obscured.
Its results should be meaningful.
Its failure causes should be easy to find.
Its maintenance shouldn't be hard.
Directly setup the database according to your tests' needs
@Test
void get_single() {
testDataManager.withUnicorn(unicorn);
var response =
restTemplate.getForEntity(
"%s/unicorns/%s".formatted(baseUrl, unicorn.id()), String.class);
var unicornData = objectMapper.readTree(response.getBody());
// …
}
@Component
public class TestDataManager {
private final JdbcTemplate jdbcTemplate;
public TestDataManager(DataSource daraSource) {
this.jdbcTemplate = new JdbcTemplate(daraSource);
}
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;
}
TestDataManager clear() {
jdbcTemplate.execute("TRUNCATE TABLE UNICORNS;");
return this;
}
}
Test code is code.
What's considered bad in production code, is bad in test code.
Its intention should be immediately obvious.
Its functionality shouldn't be obscured.
Its results should be meaningful.
Its failure causes should be easy to find.
Its maintenance shouldn't be hard.
Use a Builder class to create whatever object is required for the test
Make massive use of defaults & data generators to pre-fill the objects
@Test
void age_birthday() {
var gilly = new Unicorn(
randomUUID(),
"Gilly",
ManeColor.RED,
111,
11,
LocalDate.now().minusYears(62));
assertThat(gilly.age()).isEqualTo(62);
}
@Test
void age_birthday() {
var gilly = aUnicorn()
.dateOfBirth(LocalDate.now().minusYears(62))
.build();
assertThat(gilly.age()).isEqualTo(62);
}
public class UnicornTestDataBuilder {
private final SecureRandom random = new SecureRandom();
private UUID id = randomUUID();
private String name = "Gilly";
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.of(2000, 1, 1);
private UnicornTestDataBuilder() {}
public static UnicornTestDataBuilder aUnicorn() {
return new UnicornTestDataBuilder();
}
public UnicornTestDataBuilder id(UUID id) {
this.id = id;
return this;
}
public UnicornTestDataBuilder name(String name) {
this.name = name;
return this;
}
// …
public Unicorn build() {
return new Unicorn(id, name, maneColor, hornLength, hornDiameter, dateOfBirth);
}
}
Test code is code.
What's considered bad in production code, is bad in test code.
Its intention should be immediately obvious.
Its functionality shouldn't be obscured.
Its results should be meaningful.
Its failure causes should be easy to find.
Its maintenance shouldn't be hard.
Instead of mocks use nullables to stub infrastructure components
@Test
void getUnicorn() {
var gilly = aUnicorn().build();
var serviceMock = mock(UnicornService);
when(serviceMock.findUnicorn(anyString()))
.thenReturn(gilly)
var controller = new UnicornController(
serviceMock);
var response = controller.getUnicorn(gilly.id());
assertThat(response.getStatusCode())
.isEqualTo(HttpStatusCode.valueOf(200));
verify(serviceMock, times(1)).findUnicorn(gilly.id())
}
@Test
void getUnicorn() {
var gilly = aUnicorn().build();
var repository = UnicornRepository
.createNullable()
.add(gilly);
var controller = new UnicornController(
new UnicornService(repository));
var response = controller.getUnicorn(gilly.id());
assertThat(response.getStatusCode())
.isEqualTo(HttpStatusCode.valueOf(200));
}
Test code is code.
What's considered bad in production code, is bad in test code.
Its intention should be immediately obvious.
Its functionality shouldn't be obscured.
Its results should be meaningful.
Its failure causes should be easy to find.
Its maintenance shouldn't be hard.
By Michael Kutz
Quality Engineer at REWE digital, Conference Speaker about QA & Agile, Founder of Agile QA Cologne meetup, Freelance QA Consultant