Wraz z rozwojem aplikacji AnimalShelter natrafiam na ciekawe przypadki. Jeden z nich dotyczył klasy BeanPropertyRowMapper
służącej do mapowania wiersza bazodanowego do instancji klasy. Błąd wyszedł na jaw dopiero podczas testów integracyjnych. Wiersz, który chciałem wyciągnąć z bazy nie mapował pól do odpowiedniego obiektu. Otrzymywałem po prostu domyślne wartości w polach takie jak np. null czy 0. Przejdźmy zatem do sedna problemu.
Analizowany przypadek
Zaczynając od krótkiego wprowadzenia, jednym z zadań aplikacji jest przyjmowanie zwierząt do schroniska. Jednak nic nie jest workiem bez dna, więc i nasze schronisko ma swoje limity miejsc. Z tego powodu powstała klasa pakietowa ShelterAnimalRow
, która reprezentuje już przyjęte zwierzątko do schroniska. Dzięki niej jesteśmy w stanie pobrać aktualną listę zwierząt i sprawdzić czy mamy jeszcze miejsce, aby przyjąć kolejne pupile. Już teraz warto zwrócić uwagę, że wszystkie elementy w tej klasie ustawiłem na dostęp pakietowy: samą klasę, pola, konstruktor oraz setter (SPOILER!).
1
2
3
4
5
6
7
8
9
10
11
12
class ShelterAnimalRow {
UUID animal_id;
ShelterAnimal toShelterAnimal() {
return new ShelterAnimal(new AnimalId(animal_id));
}
void setAnimal_id(final UUID animal_id) {
this.animal_id = animal_id;
}
}
Przy okazji warto zaznaczyć, że korzystam z biblioteki vavr stąd zapis Stream.ofAll(...)
. Następnie przy pomocy JdbcTemplate
wykonywane jest zapytanie SELECT pobierające tylko te rekordy z bazy, których wartość kolumny adopted_at
jest null
. Musimy zmapować pobrane dane na klasę ShelterAnimalRow
wykorzystując właśnie BeanPropertyRowMapper
. Robimy to tworząc jego nowy obiekt używając konstruktora przyjmującego parametr typu java.lang.Class
. Kolejnym krokiem jest zmapowanie wszystkich elementów strumienia na klasę ShelterAnimal
i wrzucenie całości do zbioru.
1
2
3
4
5
6
7
8
9
10
@Override
public Set<ShelterAnimal> queryForAnimalsInShelter() {
return Stream.ofAll(
jdbcTemplate.query(
"SELECT a.animal_id FROM shelter_animal a WHERE a.adopted_at IS NULL",
new BeanPropertyRowMapper<>(ShelterAnimalRow.class)
))
.map(ShelterAnimalRow::toShelterAnimal)
.toSet();
}
Pora na test
Teraz pora przetestować nasze rozwiązanie. Wykorzystamy do tego celu test integracyjny, który będzie używał Testcontainers stawiając cały kontekst Springa. Dzięki temu odwzorujemy produkcyjne działanie aplikacji.
1
2
3
4
5
6
7
8
9
10
11
@Test
@Transactional
@DisplayName("Should fetch animals which are in shelter")
void should_fetch_animals_which_are_in_shelter(@Autowired ShelterDatabaseRepository repository) {
Animal animal = animal();
repository.save(animal);
Set<ShelterAnimal> shelterAnimals = repository.queryForAnimalsInShelter();
assertThat(shelterAnimals).containsOnly(new ShelterAnimal(animal.getId()));
}
Po uruchomieniu testu od razu dostaniemy wyjątek java.lang.IllegalArgumentException: id cannot be null
. Jest to zabezpieczenie przed stworzeniem nieprawidłowego obiektu klasy AnimalId
. Oznacza to, że zmiennej UUID animal_id
klasy ShelterAnimalRow
została przypisana wartość null
. Tylko dlaczego skoro stworzyliśmy obiekt Animal
, który ma podany losowy UUID
i zapisaliśmy go w bazie? Odpowiedź została znaleziona w dokumentacji, a dokładniej w Javadoc klasy BeanPropertyRowMapper
.
Column values are mapped based on matching the column name as obtained from result set meta-data to public setters for the corresponding properties. The names are matched either directly or by transforming a name separating the parts with underscores to the same name using “camel” case.
Wynika z tego, że klasa może być pakietowa, konstruktor i pola też, ale setter musi być PUBLICZNY! W innym przypadku dane pole po prostu nie zostanie wypełnione danymi. Robiąc szybką poprawkę test od razu przechodzi na zielono. Z dokumentacji możemy dodatkowo dowiedzieć się, że jeżeli kolumny mają podkreślenie w nazwie jak np. animal_id
to BeanPropertyRowMapper
potrafi je zmapować na pola w konwencji “camelCase”.
Podsumowanie
Najważniejsza lekcja z tego artykułu to, że zawsze trzeba uważnie czytać dokumentację. Błędy wynikające z jej nieprzestrzegania nie powinny mieć odzwierciedlenia w testach integracyjnych. Nie ma żadnej wartości biznesowej w tym, że nie mamy wiedzy jak wykorzystywać dane narzędzie. Musimy się po prostu z tym obeznać, a testy tego typu powinny chronić główne części systemu.
Przy okazji zachęcam Cię do zapoznania się z całym procesem powstawania aplikacji AnimalShelter, który opisuję w tej serii artykułów.