Michael Kutz
Quality Engineer at REWE digital, Conference Speaker about QA & Agile, Founder of Agile QA Cologne meetup, Freelance QA Consultant
Why does the quality of test code matter?
Its intention should be immediately obvious.
Test code is code.
What's considered bad in production code, is bad in test code.
Its functionality should not be obscured.
Its results should be meaningful.
Its failure causes should be easy to find.
Its maintenance should not be hard.
All tests have three parts (or less):
Tests rely on non-obvious setup mechanisms and/or test data
@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() {
var response = restTemplate
.getForEntity(baseUrl + "/unicorns", 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.
Directly setup the database according to your tests' needs
@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() {
var response = restTemplate
.getForEntity(baseUrl + "/unicorns", 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);
}
}
@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() {
testDataManager
.clean()
.withUnicorn(unicorn);
var response = restTemplate
.getForEntity(baseUrl + "/unicorns", 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);
}
}
@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;
}
}
@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() {
testDataManager
.clean()
.withUnicorn(unicorn);
var response = restTemplate
.getForEntity(baseUrl + "/unicorns", 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);
}
}
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
@Test
void ageWorksHereAlso() {
var gilly =
new Unicorn(
randomUUID(), // DOESN'T MATTER… OBVIOUSLY
"Gilly", // NOT IMPORTANT
ManeColor.RED, // JUST RANDOM
111, // NEVER MIND
11, // COULD BE ANY NUMBER
LocalDate.now().minusYears(62).plusDays(1)); // IMPORTANT
assertThat(gilly.age()).isEqualTo(61); // WHY?
}
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.
E.g. Data objects are being created with all fields
Even if the set value doesn't matter for the case
@Test
void ageWorksHereAlso() {
var gilly =
new Unicorn(
randomUUID(), // DOESN'T MATTER… OBVIOUSLY
"Gilly", // NOT IMPORTANT
ManeColor.RED, // JUST RANDOM
111, // NEVER MIND
11, // COULD BE ANY NUMBER
LocalDate.now().minusYears(62).plusDays(1)); // IMPORTANT
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.
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 ageWorksHereAlso() {
var gilly =
new Unicorn(
randomUUID(), // DOESN'T MATTER
"Gilly", // NOT IMPORTANT
ManeColor.RED, // JUST RANDOM
111, // NEVER MIND
11, // COULD BE ANY NUMBER
LocalDate.now().minusYears(62).plusDays(1)); // IMPORTANT
assertThat(gilly.age()).isEqualTo(61); // WHY 61?
}
@Test
void ageWorksHereAlso() {
var gilly = aUnicorn()
.dateOfBirth(LocalDate.now().minusYears(62))
.build();
assertThat(gilly.age()).isEqualTo(62);
}
@Test
void ageWorksHereAlso() {
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.
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;
@Test
void postNewUnicorn() {
var garryJson = aUnicorn().buildJson();
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);
}
}
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 method to avoid code duplications and make the unit under test more obvious
@SpringBootTest(webEnvironment = WebEnvironment.RANDOM_PORT, classes = LocalTestApplication.class)
class ApplicationTest {
@Value("http://localhost:${local.server.port}")
String baseUrl;
@Test
void postNewUnicorn() {
var garryJson = aUnicorn().buildJson();
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);
}
}
@SpringBootTest(webEnvironment = WebEnvironment.RANDOM_PORT, classes = LocalTestApplication.class)
class ApplicationTest {
@Autowired
FrontendActHelper frontend;
@Test
@DirtiesContext
void postNewUnicorn() {
var garryJson = aUnicorn().buildJson();
var response = frontend.postUnicorn(garryJson);
assertThat(response.getStatusCode()).isEqualTo(HttpStatusCode.valueOf(201));
assertThat(response.getHeaders().get("Location")).isNotNull().hasSize(1);
}
}
@Component
class FrontendActHelper {
@Value("http://localhost:${local.server.port}")
private String baseUrl;
public Response postUnicorn(String unicornJson) {
return restTemplate.exchange(
post("%s/unicorns/".formatted(baseUrl))
.header("Content-Type", "application/json")
.body(garryJson),
String.class);
}
}
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 {
@Autowired
FrontendActHelper frontend;
@Autowired
TestDataManager testDataManager;
@Test
void getSingleUnicornWorksAndReturnsData() throws JsonProcessingException {
var unicorn = aUnicorn().build();
testDataManager.clear().withUnicorn(unicorn);
var response = frontend.getUnicorn(unicorn.id());
var unicornData = 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.
Group long asserts that check one logical thing in verification methods
@SpringBootTest(webEnvironment = WebEnvironment.RANDOM_PORT, classes = LocalTestApplication.class)
class ApplicationTest {
@Autowired
FrontendActHelper frontend;
@Autowired
TestDataManager testDataManager;
@Autowired
ObjectMapper objectMapper
@Test
void getSingleUnicornWorksAndReturnsData() throws JsonProcessingException {
var unicorn = aUnicorn().build();
testDataManager.clear().withUnicorn(unicorn);
var response = frontend.getUnicorn(unicorn.id());
var unicornData = objectMapper.readTree(response.getBody());
assertThat(unicornData.get("name").asText()).isEqualTo("Grace");
assertThat(unicornData.get("maneColor").asText()).isEqualTo("RAINBOW");
assertThat(unicornData.get("hornLength").asInt()).isEqualTo(42);
assertThat(unicornData.get("hornDiameter").asInt()).isEqualTo(10);
assertThat(unicornData.get("dateOfBirth").asText()).isEqualTo("1982-02-19");
}
}
@SpringBootTest(webEnvironment = WebEnvironment.RANDOM_PORT, classes = LocalTestApplication.class)
class ApplicationTest {
@Autowired
FrontendActHelper frontend;
@Autowired
TestDataManager testDataManager;
@Autowired
AssertHelper assertHelper;
@Test
void getSingleUnicornWorksAndReturnsData() {
var unicorn = aUnicorn().build();
testDataManager.clear().withUnicorn(unicorn);
var response = frontend.getUnicorn(unicorn.id());
assertHelper.assertEquals(response.getBody(), unicorn)
}
}
@Component
class AssertHelper {
@Autowired
ObjectMapper objectMapper
public void assertEquals(String unicornJson, Unicorn unicorn) throws JsonProcessingException {
var unicornData = objectMapper.readTree(unicornJson);
assertThat(unicornData.get("name").asText()).isEqualTo(unicorn.name());
assertThat(unicornData.get("maneColor").asText()).isEqualTo(unicorn.maneColor());
assertThat(unicornData.get("hornLength").asInt()).isEqualTo(unicorn.age());
assertThat(unicornData.get("hornDiameter").asInt()).isEqualTo(unicorn.hornDiameter());
assertThat(unicornData.get("dateOfBirth").asText()).isEqualTo(unicorn.dateOfBirth());
}
}
@SpringBootTest(webEnvironment = WebEnvironment.RANDOM_PORT, classes = LocalTestApplication.class)
class ApplicationTest {
@Autowired
FrontendActHelper frontend;
@Autowired
TestDataManager testDataManager;
@Autowired
AssertHelper assertHelper;
@Test
void getSingleUnicornWorksAndReturnsData() {
var unicorn = aUnicorn().build();
testDataManager.clear().withUnicorn(unicorn);
var response = frontend.getUnicorn(unicorn.id());
assertHelper.assertEquals(response.getBody(), 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.
Test case names don't reflect the actual test content
Names are chosen inconsistently
@Test
void testHDNotGivenResultsIn500() {
var larryJson = aUnicornJson()
.hornDiameter(0)
.build()
var response = actHelper.postUnicorn(larryJson);
assertThat(response.getStatusCode()).isEqualTo(HttpStatus.BAD_REQUEST);
assertThat(response.getHeaders().containsKey("Location")).isFalse();
assertThat(response.getBody()).contains("hornDiameter must be between 1 and 40");
}
class UnicornApiTest {
@Test
void getUnicornsWorksAndReturnsNonEmptyList() {}
@Test
void getSingleUnicornWorksAndReturnsData() {}
@Test
void postNewUnicornShouldWork() {}
@Test
void testHLTooMuchYields400() {}
@Test
void testHDNotGivenResultsIn500() {}
}
class UnicornTest {
@Test
void ageWorks() { /* … */ }
@Test
void ageWorksHereToo() { /* … */ }
@Test
void ageWorksHereAlso() { /* … */ }
@Test
void negativeAge() { /* … */ }
}
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.
Apply a consistent test case naming scheme
Important info about a test:
<expectedBehavior>
<stateUnderTest>
<unitUnderTest>
<ClassUnderTest> +
<methodUnderTest>
class <ClassUnderTest>Test {
void test<methodUnderTest>Should<expectedBehavior>With<stateUnderTest>() { … }
}
Important info about a test:
<expectedBehavior>
<stateUnderTest>
<unitUnderTest>
<ClassUnderTest> +
<methodUnderTest>
class <ClassUnderTest>Test {
void test<methodUnderTest>Should<expectedBehavior>With<stateUnderTest>() { … }
void <methodUnderTest>_<stateUnderTest>_<expectedBehavior>() { … }
}
Important info about a test:
<expectedBehavior>
<stateUnderTest>
<unitUnderTest>
<ClassUnderTest> +
<methodUnderTest>
class <ClassUnderTest>Test {
void test<methodUnderTest>Should<expectedBehavior>With<stateUnderTest>() { … }
void <methodUnderTest>_<stateUnderTest>_<expectedBehavior>() { … }
void <methodUnderTest>_<stateUnderTest>() {
… // see assert code for <expectedBehavior>
}
}
Important info about a test:
<expectedBehavior>
<stateUnderTest>
<unitUnderTest>
<ClassUnderTest> +
<methodUnderTest>
class <ClassUnderTest>Test {
void test<methodUnderTest>Should<expectedBehavior>With<stateUnderTest>() { … }
void <methodUnderTest>_<stateUnderTest>_<expectedBehavior>() { … }
void <methodUnderTest>_<stateUnderTest>() {
… // see assert code for <expectedBehavior>
}
void <methodUnderTest>Test.test<Counter>() {
… // see arrange code for <stateUnderTest>
… // see assert code for <expectedBehavior>
}
}
Important info about a test:
<expectedBehavior>
<stateUnderTest>
<unitUnderTest>
<ClassUnderTest> +
<methodUnderTest>
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 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() {}
}
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() {}
}
class <ClassUnderTest>Test {
void <methodUnderTest>_<stateUnderTest>() {
… // see code for <expectedBehavior>
}
}
class UnicornApiTest {
@Test
void getUnicornsWorksAndReturnsNonEmptyList() {}
@Test
void getSingleUnicornWorksAndReturnsData() {}
@Test
void postNewUnicornShouldWork() {}
@Test
void testHLTooMuchYields400() {}
@Test
void testHDNotGivenResultsIn500() {}
}
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() {}
}
class <ClassUnderTest>Test {
void <methodUnderTest>_<stateUnderTest>() {
… // see code for <expectedBehavior>
}
}
class UnicornApiTest {
@Test
void GET_unicorns() {}
@Test
void GET_unicorn() {}
@Test
void POST_unicorn() {}
@Test
void POST_unicorn_invalid_hornLength() {}
@Test
void POST_unicorn_invalid_hornDiameter() {}
}
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() {}
}
class <ClassUnderTest>Test {
void <methodUnderTest>_<stateUnderTest>() {
… // see code for <expectedBehavior>
}
}
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
UnicornController |
---|
getUnicorn(id: String): Response |
UnicornService |
---|
findUnicorn(id: String): Unicorn |
UnicornRepository |
---|
findById(id: String): Unicorn |
UnicornControllerTest |
---|
getUnicorn() |
UnicornController |
---|
getUnicorn(id: String): Response |
UnicornControllerTest |
---|
getUnicorn() |
MOCK
@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())
}
UnicornController |
---|
getUnicorn(id: String): Response |
UnicornControllerTest |
---|
getUnicorn() |
MOCK
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())
}
UnicornController |
---|
getUnicorn(id: String): Response |
UnicornControllerTest |
---|
getUnicorn() |
MOCK
@Test
void getUnicorn() {
var gilly = aUnicorn().build();
var repository = UnicornRepository
.createNullable()
.save(gilly);
var controller = new UnicornController(
new UnicornService(repository));
var response = controller.getUnicorn(gilly.id());
assertThat(response.getStatusCode())
.isEqualTo(HttpStatusCode.valueOf(200));
}
UnicornController |
---|
getUnicorn(id: String): Response |
UnicornControllerTest |
---|
getUnicorn() |
UnicornService |
---|
findUnicorn(id: String): Unicorn |
UnicornRepositoryNullable |
---|
findById(id: String): 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.
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.
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.
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.
By Michael Kutz
Quality Engineer at REWE digital, Conference Speaker about QA & Agile, Founder of Agile QA Cologne meetup, Freelance QA Consultant