Czym byłby dzień z życia programisty bez problemów? Na pewno nie tym samym co obecnie przeżywamy. W tym artykule chciałbym opisać wyzwania (sic!) jakie napotkałem podczas ostatnich prac nad AnimalShelter. Żeby nie było tak dołująco opiszę też kilka założeń jakie przyjąłem sobie podczas dewelopmentu. Nie przedłużając zacznijmy od wysokiego C, czyli od problemu ze Spring Data JDBC.

Problem z Spring Data JDBC

Z racji decyzji o podłączeniu dwóch baz danych do aplikacji musiałem się sporo namęczyć podczas przejścia ze Spring Data JPA na Spring Data JDBC. Co prawda teraz praktycznie wszędzie wykorzystuję JdbcTemplate, ale w jednym miejscu zdecydowałem się na użycie CrudRepository. Było to podyktowane tym, że chcę kiedyś obsługiwać wiele schronisk i przy jednym z zapytań trzeba będzie łączyć dwie tabele. Można byłoby później napisać SQL wykorzystujący FETCH, ale chciałem przy okazji nauczyć się czegoś nowego.

Samo przejście było gładkie i przyjemne. Wszystko wyglądało super dopóki nie włączyłem aplikacji. Wtedy jedno z zapytań zamiast korzystać z bazy danych shelter-proposal uderzało do shelter-catalogue. Przyczyną okazało się wykorzystanie adnotacji @Primary do konfiguracji beanów w module catalogue. To one rejestrowały się wszędzie tam, gdzie nie wskazałem kwalifikatora. Odbywało się to nawet dla modułu adoption. Natomiast po usunięciu tych adnotacji kontekst Springa nie chciał wstać, ponieważ teraz rejestrowały się dwa identyczne beany niezbędne Springowi do utworzenie własnej, domyślnej konfiguracji.

Traciłem nadzieję, kiedy nie mogłem znaleźć nic konkretnego w tym temacie w Internecie. Jednak nagle trafiłem na podobny błąd zgłoszony na GitHubie. Okazało się, że nie tylko ja borykam się z takim problemem. Co prawda to zgłoszenie wisi już od 3 lat w statusie Open, ale użytkownicy w komentarzach podali obejście tego błędu. W moim przypadku zadziałało rozwiązanie zaprezentowane przez użytkownika kota65535. Polega ono na zarejestrowaniu wielu dodatkowych beanów per baza danych. Niezbędne jest również współdzielenie jeszcze 3 beanów przez wszystkie moduły: JdbcCustomConversions, Dialect oraz JdbcMappingContext (ustawia w nich ten sam dialekt DBMS). W moim przypadku nie jest to problemem, bo obydwie bazy danych stoją na MySQL. Nie wiem natomiast jak aplikacja zachowywałaby się w innej sytuacji.

Najbardziej w tym rozwiązaniu boli mnie to, że znowu jeden z beanów musi mieć adnotację @Primary. Chodzi tutaj o JdbcConverter. Do tej pory to nie powoduje żadnych problemów, ale zobaczymy jak sytuacja będzie wyglądała w przyszłości.

Teraz mądrzejszy o to doświadczenie wiem, że lepiej na starcie oprzeć się o jedną bazę danych, ale na różnych schematach. Później łatwiej jest wydzielić oddzielne aplikacje z takiego rozwiązania, a my mamy mniej problemów technicznych na głowie.

ADRy to ważna rzecz

Jeśli w zespole podejmujecie ważną decyzję architektoniczną to warto na koniec dyskusji spisać o niej notatkę. W ten sposób będziecie pamiętać o tym, dlaczego właściwie zdecydowaliście się na takie rozwiązania a nie inne. Będziecie wiedzieć jakie były argumentu za i przeciw. Taka notatka nosi nazwę ADR, czyli Architectural Decision Record. Jeśli jeszcze z tego nie korzystacie to zacznijcie. Naprawdę warto. Nie umknie Wam wtedy żadna z istotnych rzeczy. Nie będziecie musieli sobie przypominać, dlaczego została podjęta właśnie ta decyzja. Wystarczy tylko otworzyć wybrany dokument i wszystko staje się jasne.

Dlaczego to tym piszę? Bo sam zacząłem sobie coś takiego notować w ramach swojej aplikacji. Na razie mam tylko jeden wpis na temat wyboru testowej bazy danych, ale liczę, że powstanie ich więcej. Moja notatka wygląda następująco. Nie jest może rozbudowana, ale jest moją “drugą pamięcią”.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
# Writing integration tests

## Status

Accepted

## Context

We need to decide how we will test our integration layer in application in context of database.
On production, we use MySQL, so we consider the below solutions:
- H2
- Testcontainers with MySQL

## Decision
Test database: H2

We decided to use H2, because in our application so far there are no such complicated SQL queries and they 
are not depended on specified DBMS.

## Consequences

### Pros
Tests are much quicker.

### Cons
We might not find bug in specific queries for MySQL.

Jeśli jesteś bardziej zainteresowany bądź zainteresowana tym tematem to zapraszam do artykułu Dawida Kotarby. Napisał on wiele argumentów za stosowaniem tego podejścia, aby przekonać nieprzekonanych.

Testy integracyjne

Tak jak zostało to zawarte w notatce w poprzednim akapicie. Podszedłem do testów integracyjnych pragmatycznie. Nie chcę zbyt długo oczekiwać na ich wykonywanie. Z tego powodu zdecydowałem się właśnie na zastosowanie bazy danych H2.

Znam minusy tego podejścia, ale jednak nie są dla mnie w tym momencie szkodliwe. Aplikacja jest mała, zapytania SQL są proste, więc nie mam się czego obawiać. Próbowałem podejść do wykorzystania Testcontainers w testach integracyjnych jednak przy ich uruchomieniu musiałem chwilę odczekać zanim środowisko było gotowe. Trwało to zbyt długo w porównaniu do podejścia z H2.

Jeśli chodzi o samą integrację to wykorzystałem podejście z wcześniej już przytaczanego ddd-by-examples/library. Polega ono na tym, aby zweryfikować tylko zewnętrzną komunikację z aplikacją, która może nastąpić przez HTTP albo bazę danych. Nie ma żadnego sprawdzenia logiki biznesowej. Według mnie jest to dobre podejście, ale ma pewną wadę. Może nam się stworzyć wiele kontekstów Springowych. Na ten moment w moim przypadku wstaje ich aż 4 podczas budowaniu projektu.

Dlaczego tak się dzieje? Dwa testy mogą być oparte o tą samą konfigurację, ale nowy kontekst może powstać przez wykorzystanie w jednym z testów mocka w postaci @MockBean.

1
2
3
4
5
6
7
8
9
10
@ExtendWith(SpringExtension.class)
@ContextConfiguration(classes = [ShelterTestContext.class, ProposalTestContext.class])
class ShelterControllerIT extends Specification {

  @Autowired
  MockMvc mvc

  // ...

}
1
2
3
4
5
6
7
8
9
10
11
12
13
@ExtendWith(SpringExtension.class)
@ContextConfiguration(classes = [ShelterTestContext.class, ProposalTestContext.class])
class ProposalControllerIT extends Specification {

  @Autowired
  MockMvc mvc

  @MockBean
  AcceptingProposal acceptingProposal

  // ...

}

Dla klasy ShelterControllerIT utworzy się kompletnie inny kontekst niż dla ProposalControllerIT. Jest to spowodowane tylko i wyłącznie tym, że w drugim przypadku postanowiliśmy zrobić zaślepkę na AcceptingProposal. Trzeba naprawdę uważać na ten zabieg, aby nie spowolnić sobie budowania projektu. Albo stosować go po prostu świadomie.

Raporty z testów

Przydatnym efektem ubocznym pisania testów jest możliwość wygenerowania raportu z ich wykonania. Takie zestawienie możemy zaprezentować osobie biznesowej i zweryfikować czy właśnie o to chodziło w danej procedurze. Jeśli piszemy testy w Spocku to uzyskanie raportu nie jest trudne. Wystarczy dodać odpowiednią zależność do projektu i tyle. Chociaż w AnimalShelter musiałem zapisać również jeden plik konfiguracyjny, ale o tym za chwilę. O jaką właściwie zależność chodzi?

Na GitHubie możemy znaleźć projekt Renato Athaydes o jakże oczywistej nazwie spock-reports. Według autora należy tylko dodać odpowiedni wpis do narzędzia budującego. Nie jest niezbędny żaden dodatkowy wysiłek. Trzeba tylko uważać, aby zgrać ze sobą odpowiednie wersje Javy, Groovy, Spock oraz spock-reports. Jak wspomniałem wcześniej, ja musiałem jeszcze zamieścić plik o nazwie com.athaydes.spockframework.report.IReportCreator.properties w katalogu META-INF/services w testowym resources o następującej treści.

# Name of the implementation class(es) of report creator(s) to enable (separate multiple entries with commas)
# Currently supported classes are:
#   1. com.athaydes.spockframework.report.internal.HtmlReportCreator
#   2. com.athaydes.spockframework.report.template.TemplateReportCreator
com.athaydes.spockframework.report.IReportCreator=com.athaydes.spockframework.report.internal.HtmlReportCreator

# Set properties of the report creator
# For the HtmlReportCreator, the only properties available are
# (the location of the css files is relative to the classpath):
com.athaydes.spockframework.report.internal.HtmlReportCreator.featureReportCss=spock-feature-report.css
com.athaydes.spockframework.report.internal.HtmlReportCreator.summaryReportCss=spock-summary-report.css
com.athaydes.spockframework.report.internal.HtmlReportCreator.printThrowableStackTrace=false
com.athaydes.spockframework.report.internal.HtmlReportCreator.inlineCss=true
com.athaydes.spockframework.report.internal.HtmlReportCreator.enabled=true
# options are: "class_name_and_title", "class_name", "title"
com.athaydes.spockframework.report.internal.HtmlReportCreator.specSummaryNameOption=class_name_and_title

# exclude Specs Table of Contents
com.athaydes.spockframework.report.internal.HtmlReportCreator.excludeToc=false

# Output directory (where the spock reports will be created) - relative to working directory
com.athaydes.spockframework.report.outputDir=target/spock-reports

# Output directory where to store the aggregated JSON report (used to support parallel builds)
com.athaydes.spockframework.report.aggregatedJsonReportDir=

# If set to true, hides blocks which do not have any description
com.athaydes.spockframework.report.hideEmptyBlocks=false

# Set the name of the project under test so it can be displayed in the report
com.athaydes.spockframework.report.projectName=

# Set the version of the project under test so it can be displayed in the report
com.athaydes.spockframework.report.projectVersion=Unknown

# Show the source code for each block
com.athaydes.spockframework.report.showCodeBlocks=false

# Set the root location of the Spock test source code (only used if showCodeBlocks is 'true')
com.athaydes.spockframework.report.testSourceRoots=src/test/groovy

# Set properties specific to the TemplateReportCreator
com.athaydes.spockframework.report.template.TemplateReportCreator.specTemplateFile=/templateReportCreator/spec-template.md
com.athaydes.spockframework.report.template.TemplateReportCreator.reportFileExtension=md
com.athaydes.spockframework.report.template.TemplateReportCreator.summaryTemplateFile=/templateReportCreator/summary-template.md
com.athaydes.spockframework.report.template.TemplateReportCreator.summaryFileName=summary.md
com.athaydes.spockframework.report.template.TemplateReportCreator.enabled=true

W tym momencie przy wywołaniu mvn clean install wszystko działa jak powinno. Raport z testów tworzy się w ścieżce target/spock-reports/index.html i prezentuje się następująco.

Wszystkie testy w Spock Reports
Wszystkie testy w Spock Reports

Szczegółowy podgląd testu w Spock Reports
Szczegółowy podgląd testu w Spock Reports

Wszystko jest ładne i przejrzyste przez co bez problemu zapozna się z nimi nawet osoba nietechniczna. Oczywiście nazwy typu ‘Prepare any proposal id’ odpowiadają etykietom ze Spocka np. dla given.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
def 'should handle proposal process'() {
  given: "Prepare any proposal id."
    ProposalId proposalId = anyProposalId()
  when: "Animal has been confirmed."
    catalogueHandler.handle(animalConfirmedNow(AnimalId.of(proposalId.getValue())))
  and: "Proposal is accepted."
    shelterHandler.handle(proposalAcceptedNow(proposalId))
  and: "Proposal is canceled."
    shelterHandler.handle(proposalCanceledNow(proposalId))
  and: "Proposal is accepted once again."
    shelterHandler.handle(proposalAcceptedNow(proposalId))
  and: "Proposal is accepted one more time."
    shelterHandler.handle(proposalAcceptedNow(proposalId))
  then: "Operation of accepting the same proposal is forbidden."
    1 * publisher.publish(_ as ProposalAcceptanceFailed)
}

Podsumowanie

Mam nadzieję, że ten wpis dostarczył Ci kilka ciekawych wskazówek oraz ciekawostek. Na koniec chciałem jeszcze dodać, że dorobiłem też mechanizm archiwizacji procesu akceptacji wniosków, który znajduje się w warstwie infrastruktury. Przed każdą zmianą stanu wniosku dokonywany jest wpis do tabeli archiwizującej poprzednie operacje. Dodatkowo zastanawiam się nad samym procesem akceptacji danego wniosku przez schronisko. Być może podejdę do niego podobnie jak w przypadku modułu catalogue, ale to już dokładniej poruszę w kolejnych wpisach.

Link do GitHub: https://github.com/cezarysanecki/animal-shelter