Wydaje się, że testy automatyczne zagościły w świecie wytwarzania oprogramowania na dobre. Jednak jeśli są one niedostatecznej jakości, szybko mogą stać się obciążeniem przynoszącym więcej szkody niż pożytku. Podczas prezentacji omówię najczęstsze błędy popełniane w testach jednostkowych i integracyjnych przez średnio zaawansowanych praktyków Test-Driven Development. Pokażę problemy wynikające z nieznajomości lub niewłaściwego wykorzystania narzędzi przede wszystkim z rodziny JUnit. Wytłumaczę również pojęcie betonowania oraz kwestię, jak poprawić swoje testy bazy danych i API REST-owych.
6. Przykład początkowy
@Test
public void testShouldWork() {
assertTrue(validator.isValid(
dateTime("2015-02-19 15:00"),
dateTime("2015-02-20 15:00")));
}
7. Zmiana nazwy
@Test
public void shouldBeValidWhenStartDateIsBeforeEndDate() {
assertTrue(validator.isValid(
dateTime("2015-02-19 15:00"),
dateTime("2015-02-20 15:00")));
}
8. Struktura testu
@Test
public void shouldBeValidWhenStartDateIsBeforeEndDate() {
DateTime startDate = dateTime("2015-02-19 15:00");
DateTime endDate = dateTime("2015-02-20 15:00");
boolean valid = validator.isValid(startDate, endDate);
assertTrue(valid);
}
9. Given / When / Then
@Test
public void shouldBeValidWhenStartDateIsBeforeEndDate() {
// given
DateTime startDate = dateTime("2015-02-19 15:00");
DateTime endDate = dateTime("2015-02-20 15:00");
// when
boolean result = validator.isValid(startDate, endDate);
// then
assertTrue(result);
}
✓
10. Inicjalizacja zmiennych
User user = new User();
user.setId(1);
user.setEnabled(true);
user.setFirstName("Jan");
user.setLastName("Kowalski");
Address address = new Address();
address.setCity("Gliwice");
address.setStreet("Akademicka");
address.setNumber(16);
user.setAddress(address);
// albo
User user = new User(1, true, "Jan", "Kowalski", new
Address("Gliwice" , "Akademicka", 16));
11. Buildery
User user = UserBuilder.anUser()
.withId(1)
.enabled()
.withFirstName("Jan")
.withLastName("Kowalski")
.livesIn(AddressBuilder.anAddress()
.inCity("Gliwice")
.onStreet("Akademicka")
.atNumber(16).build()
).build();
✓
15. Pętle
@Test
public void shouldBeValidWhenStartDateIsBeforeEndDate() {
// given
LocalDateTime endDate = dateTime("2015-02-20 15:00");
for (int i = 16; i <= 19; i++) {
LocalDateTime startDate = dateTime("2015-02-" + i + " 15:00");
// when
boolean result = validator.isValid(startDate, endDate);
// then
assertTrue(result);
}
}
16. Data-Driven Tests
@Test
@Parameters(method="nullDates")
public void shouldBeValidWhenAnyDateIsNull(
DateTime startDate, DateTime endDate) {
// when
boolean result = validator.isValid(startDate, endDate);
// then
assertTrue(result);
}
private Object nullDates() {
return $($(null, dateTime("2015-02-19 15:00")),
$(dateTime("2015-02-19 15:00"), null),
$(null, null));
}
17. Spock
def "be valid when any of dates is null"() {
expect:
validator.isValid(startDate, endDate)
where:
startDate | endDate
null | null
dateTime("2015-02-20 15:00") | null
null | dateTime("2015-02-20 15:00")
}
✓
18. Mocki
public void shouldFilterOutDeletedItems() {
// given
Item deleted = ItemBuilder.withId(1).deleted().build();
Item active = ItemBuilder.withId(2).build();
when(itemsRepository.getAllItems()).thenReturn(
newArrayList(deleted, active));
// when
List<Item> activeItems =
itemsService.getActiveItems();
// then
assertThat(activeItems).containsExactly(active);
}
✓
23. Given
Discount discount = null;
int userId = 0;
User user = null;
Item item1 = new Item(user, 0);
Item item2 = mock(Item.class);
…
24. Przewaga testów integracyjnych
• Są krótsze
• Odporne na zmiany w implementacji
• Sprawdzają rzeczywistość
• Pozwalają na eksperymenty
25. Test integracyjny
// given
Discount discount = new Discount(20);
User user = UserBuilder.withId(123).build();
Item item1 = ItemBuilder.withId(1).soldByUser(user).withPrice(100).build();
Item item2 = ItemBuilder.withId(2).soldByUser(user).withPrice(200).build();
userRepository.save(user);
itemsRepository.saveAll(item1, item2);
// when
discountService.applyDiscount(user.getId(), discount);
// then
assertThat(itemsRepository.findItems(user))
.extracting("price").containsExactly(80, 160);