2. Agenda
● Unit Tests Naming Convention
● Test structure
● What Makes a Good Test?
● Cleanly Create Test Data
● Keep Cause and Effect Clear
● Don't Put Logic in Tests
● Keep Tests Focused
● Test Behaviors, Not Methods
● Prefer Testing Public APIs Over Private Methods/Implementation-Detail Classes
3. Unit Tests class Naming Convention
[The name of the tested class]Test
UserServiceRepository
UserServiceRepositoryTest
4. Integration Tests class Naming Convention
[The name of the tested feature]Test
RegistrationTest
5. Naming Test Methods If we write tests for a single class
If we write tests for a single class
[the name of the tested method]_[expected input / tested state]_[expected
behavior]
registerNewUserAccount()
registerNewUserAccount_ExistingEmailAddressGiven_ShouldThrowException()
6. Naming Test Methods If we write tests for a single feature
[the name of the tested feature]_[expected input / tested state]_[expected
behavior]
registerNewUserAccount_ExistingEmailAddressGiven_ShouldShowErrorMessage()
7. We should find answers to following questions
● What are the features of our application?
● What is the expected behavior of a feature or method when it receives an
input X?
● Also, if a test fails, we have a pretty good idea what is wrong before we read
the source code of the failing test.
8. What Makes a Good Test?
Clarity - Ясность
Completeness - Полнота
Conciseness - Лаконичность
Resilience - Устойчивость
11. Test structure
@Test
public void resetPassword_userWithPassword_shouldSetTmpPassword() {
// prepare or given
User user = new User().setPassword("lost password");
//act or when
userService.resetPassword(user);
//verify or then
assertThat(user.getTmpPassword()).isNotEmpty();
}
12. Could you find expected result?
// Don't
ProductDTO product1 = requestProduct(1);
ProductDTO product2 = new ProductDTO("1", List.of(State.ACTIVE, State.REJECTED))
assertThat(product1).isEqualTo(product2);
13. Use the Prefixes “actual*” and “expected*”
// Do
ProductDTO actualProduct = requestProduct(1);
ProductDTO expectedProduct = new ProductDTO("1", List.of(State.ACTIVE, State.REJECTED))
assertThat(actualProduct).isEqualTo(expectedProduct); // nice and clear.
14. What Makes a Good Test?
@Test
void calculateTotal_multipleItems_happyPath() {
ShoppingCart shoppingCart = new ShoppingCart(new DefaultRoundingStrategy(),
"unused", NORMAL, false, false, TimeZone.getTimeZone("UTC"), null);
int totalPrice = shoppingCart.calculateTotal(
newItem1(),
newItem2(),
newItem3());
assertThat(totalPrice).isEqualTo(25); // Where did this number come from?
}
16. How easy is it to read this test?
@Test
public void categoryQueryParameter() throws Exception {
List<ProductEntity> products = ImmutableList.of(
new ProductEntity().setId("1").setName("Envelope").setCategory("Office")....,
new ProductEntity().setId("2").setName("Pen").setCategory("Office")....,
new ProductEntity().setId("3").setName("Notebook").setCategory("Hardware")...
);
for (ProductEntity product : products) {
template.execute(createSqlInsertStatement(product));
}
String responseJson = mockMvc.perform(get("/products?category=Office"))
.andExpect(status().is(200))
.andReturn().getResponse().getContentAsString();
assertThat(toDTOs(responseJson))
.extracting(ProductDTO::getId)
.containsOnly("1", "2");
}
18. … but helper methods can grow over time
// This helper method starts with just a single parameter:
Company company = newCompany(Type.PUBLIC);
// But soon it acquires more and more parameters.
Company small = newCompany(2, 2, null, Type.PUBLIC);
Company privatelyOwned = newCompany(null, null, null, Type.PRIVATE);
Company bankrupt = newCompany(null, null, PAST_DATE, Type.PUBLIC);
// Or a new method is added each time a test needs a different combination of //
fields
Company small = newCompanyWithEmployeesAndBoardMembers(2,2, Type.PUBLIC);
Company privatelyOwned = newCompanyWithType(Type.PRIVATE);
Company bankrupt = newCompanyWithBankruptcyDate(PAST_DATE, Type.PUBLIC);
19. Use the test data builder pattern
Company small = newCompany().employeesCount(2).boardMembersCount(2).build();
Company privatelyOwned = newCompany().type(Type.PRIVATE).build();
Company bankrupt = newCompany().bankruptcyDate(PAST_DATE).build();
Company arbitraryCompany = newCompany().build();
// Zero parameters make this method reusable for different variations of
Company.
private static Company.CompanyBuilder newCompany() {
return Company.builder().type(Type.PUBLIC); // Set required fields
20. Never rely on default values that are specified by a helper
method
private static Company.CompanyBuilder newCompany() {
return Company.builder().type(Type.PUBLIC); // Set required fields
}
// This test needs a public company, so explicitly set it.
// It also needs a company with no board members, so explicitly clear it.
Company publicNoboardMembers = newCompany()
.type(Type.PUBLIC)
.boardMembersCount(0).build();
22. Can you tell if this test is correct?
207: @Test
208: public void testIncrement_existingKey() {
209: assertThat(counter.get("key1")).isEqualTo(9);
210: }
23. It’s impossible to say without seeing the whole picture
private final Counter counter = new Counter();
6: @BeforeEach
7: public void setUp() {
8: counter.increment("key1", 8);
9: counter.increment("key2", 100);
10: counter.increment("key1", 0);
11: counter.increment("key1", 1);
12: }
// 200 lines away
210: @Test
210: public void testIncrement_existingKey() {
210: assertThat(counter.get("key1")).isEqualTo(9);
210: }
24. Write tests where the effects immediately follow the causes
private final Counter counter = new Counter();
@Test
public void increment_newKey_happyPath() {
counter.increment("key2", 100);
assertThat(counter.get("key2")).isEqualTo(100);
}
@Test
public void increment_existingKey_happyPath() {
counter.increment("key1", 8);
counter.increment("key1", 1);
assertThat(counter.get("key1")).isEqualTo(9);
}
26. Does this test look correct?
@Test
public void getPhotosPageUrl() {
String baseUrl = "http://photos.google.com/";
UrlBuilder urlBuilder = new UrlBuilder(baseUrl);
String photosPageUrl = urlBuilder.getPhotosPageUrl();
assertThat(photosPageUrl).isEqualTo(baseUrl + "/u/0/photos");
}
27. What happens if we simplify the test by inlining the
variable?
@Test
public void getPhotosPageUrl_happyPath() {
UrlBuilder urlBuilder = new UrlBuilder("http://photos.google.com/");
String photosPageUrl = urlBuilder.getPhotosPageUrl();
assertThat(photosPageUrl).isEqualTo("http://photos.google.com//u/0/photos");
}
29. KISS - Keep it stupid simple
KISS > DRY (Don’t Repeat Yourself)
KISS > code flexibility
When tests do need their own logic, such logic should often be moved out of the test bodies and into utilities and
helper functions and often with their own tests
31. What scenario does the following code test?
@Test
void withdrawFromAccount() {
Transaction transaction = account.deposit(usd(5));
assertThat(account.withdraw(usd(5))).isEqualTo(isOk());
assertThat(account.withdraw(usd(1))).isEqualTo(isRejected());
account.setOverdraftLimit(usd(1));
assertThat(account.withdraw(usd(1))).isEqualTo(isOk());
}
32. What scenario does the following code test?
@Test
void withdrawFromAccount() {
Transaction transaction = account.deposit(usd(5));
assertThat(account.withdraw(usd(5))).isEqualTo(isOk());
assertThat(account.withdraw(usd(1))).isEqualTo(isRejected());
account.setOverdraftLimit(usd(1));
assertThat(account.withdraw(usd(1))).isEqualTo(isOk());
}
It tests three scenarios, not one!
33. Exercise each scenario in its own test!
@Test
void withdraw_canWithdrawWithinLimits() {
depositAndSettle(usd(5));
assertThat(account.withdraw(usd(5))).isEqualTo(isOk());
}
@Test
void withdraw_withdrawOverLimits_shouldBeRejected() {
depositAndSettle(usd(5));
assertThat(account.withdraw(usd(6))).isEqualTo(isRejected());
}
@Test
void withdraw_canOverdrawUpToOverdraftLimit() {
depositAndSettle(usd(5));
account.setOverdraftLimit(usd(1));
assertThat(account.withdraw(usd(6))).isEqualTo(isOk());
}
35. Test that verifies an entire method
@Test
public void testResetPassword() {
User user = new User().setPassword("lost password");
userService.resetPassword(user);
assertThat(user.getPassword()).isEmpty();
assertThat(user.getMailbox().getMessages().get(0).getTitle())
.isEqualTo("Password reset");
assertThat(user.getMailbox().getMessages().get(0).getBody())
.startsWith("You have requested password reset");
assertThat(counter.get("reset password")).isEqualTo(1);
}
36. Separate tests to verify separate behaviors
@Test
public void resetPassword_userWithPassword_usersPasswordShouldBecomeEmpty() {
User user = new User().setPassword("1234");
userService.resetPassword(user);
assertThat(user.getPassword()).isEmpty();
}
@Test
public void resetPassword_userWithPassword_userShouldReceiveEmail() {
User user = new User().setPassword("1234");
userService.resetPassword(user);
assertThat(user.getMailbox().getMessages().get(0).getTitle())
.isEqualTo("Password reset");
assertThat(user.getMailbox().getMessages().get(0).getBody())
.startsWith("You have requested password reset");
}
38. What makes this test fragile?
@Test
public void displayGreeting_showSpecialGreetingOnNewYearsDay() {
clock.setTime(NEW_YEARS_DAY);
user.setName("Frank Sinatra");
userGreeter.displayGreeting();
verify(userPrompter).updatePrompt(
"Hi Frank Sinatra! Happy New Year!",
TitleBar.of("2019-01-01"),
PromptStyle.NORMAL
);
39. Only verify arguments related to specific behavior being
tested
@Test
public void displayGreeting_showSpecialGreetingOnNewYearsDay2() {
clock.setTime(NEW_YEARS_DAY);
user.setName("Frank Sinatra");
userGreeter.displayGreeting();
verify(userPrompter)
.updatePrompt(eq("Hi Frank Sinatra! Happy New Year!"), any(), any());
}
Arguments ignored in one test can be verified in other tests. Following this pattern allows us to verify only one
behavior per test
41. Should this classes be tested separately?
class UserInfoValidator {
void validate(UserInfo info) throws ValidationException {
if(info.getDateOfBirth().isInFuture()) {
throw new ValidationException("Invalid date of birth");
}
}
}
public class UserInfoService {
private UserInfoValidator validator;
public void save(UserInfo info) {
validator.validate(info);
writeToDatabase(info);
}
}
42. Materials
● Modern Best Practices for Testing in Java, link
● Writing Clean Tests – Naming Matters, link
● Testing on the Toilet: Cleanly Create Test Data, link
● Testing on the Toilet: Keep Cause and Effect Clear, link
● Testing on the Toilet: Don't Put Logic in Tests, link
● Testing on the Toilet: Keep Tests Focused, link
● Testing on the Toilet: Test Behaviors, Not Methods, link
● Testing on the Toilet: Prefer Testing Public APIs Over Implementation-Detail
Classes, link
● Testing on the Toilet: Only Verify Relevant Method Arguments, link