Adnotacja @Transactional w testach

0

Czy w testach powinno używać się adnotacji @Transactional? Musiałem ją dodać w tym teście, bo inaczej w metodzie findEventToEdit, konkretnie w linii if (!currentUser.equals(event.getOrganizer())) { leciał LazyInitializationException. Encje Event i AppUser są w relacji jednokierunkowej OneToOne. Teraz odkąd zacząłem używać w testach 2 plików sql - pierwszy do dodawania danych testowych przed każdym testem, drugi do czyszczenia tabel po każdym teście to mam nowy problem - metoda getReferenceById zwraca mi obiekt AppUser$HibernateProxy... zamiast AppUser, w którym wszystkie pola są null. Usunięcie adnotacji @Transactional z testu pomaga, ale wtedy leci wcześniej wspomniany wyjątek. W czym jest problem?

@Test
@Transactional
void shouldReturnEventToEdit() {
    // when
    var eventToEdit = organizerEventService.findEventToEdit(appUserRepository.getReferenceById(2L), 5L);
    // then
    assertThat(eventToEdit.getName()).isEqualTo("Java Dev Talks #5");
    assertThat(eventToEdit.getEventImageData()).isNotBlank();
    assertThat(eventToEdit.getEventImage()).isNull();
    assertThat(eventToEdit.getEventType()).isEqualTo(MEETING);
    assertThat(eventToEdit.getDateTime()).isEqualTo(TOMORROW.plusMonths(1).format(DateTimeFormatter.ISO_LOCAL_DATE_TIME));
    assertThat(eventToEdit.getLanguage()).isEqualTo("polski");
    assertThat(eventToEdit.getAdmission()).isEqualTo(FREE);
    assertThat(eventToEdit.getCity()).isEqualTo("Rzeszów");
    assertThat(eventToEdit.getLocation()).isEqualTo("WSIiZ");
    assertThat(eventToEdit.getAddress()).isEqualTo("Sucharskiego 2, 35-225 Rzeszów");
    assertThat(eventToEdit.getDescription()).isEqualTo("Spotkanie rzeszowskiej grupy pasjonatów języka Java.");
}
@Transactional
public EventEditDTO findEventToEdit(AppUser currentUser, Long id) {
    Optional<Event> eventOpt = eventRepository.findById(id);
    if (eventOpt.isPresent()) {
        Event event = eventOpt.get();
        if (!currentUser.equals(event.getOrganizer())) {
            throw new AccessDeniedException("Access is denied");
        }

        return EventEditDTOMapper.mapToEventEditDTO(event);
    }

    throw new EventNotFoundException("Event with ID " + id + " not found");
}
1

Nie ważne jest czy "się powinno" czy "nie powinno". Istotne jest jakie to ma konsekwencje.

Zalety użycia @Transactional w teście:

  • Na ten moment testy się odpalają

Wady:

  • Przywiązanie do frameworka
  • Słaby design testów, bo taką adnotacją trudniej jest kontrolować w testach co się dzieje - trudno jest z czymś takim łatwo wydzielić np helper.
  • Nie można odpalić testów bez springa (i np wstrzyknąć fejkowej implementacji)

Rozważ wady i zalety, i sam zdecyduj co jest dla Ciebie lepszym wyjściem.

0

Możesz dodać do Encji: @OneToOne(fetch = FetchType.EAGER). I chyba wtedy możesz wywalić @Trasactonal z testu, ale teraz z głowy piszę
Natomiast zastanowiłbym się nad tym, czy zamiast referencji nie lepiej trzymać Id tego drugie obiektu

0
Black007 napisał(a):

Możesz dodać do Encji: @OneToOne(fetch = FetchType.EAGER). I chyba wtedy możesz wywalić @Trasactonal z testu, ale teraz z głowy piszę
Natomiast zastanowiłbym się nad tym, czy zamiast referencji nie lepiej trzymać Id tego drugie obiektu

i wtedy w teście, możesz uderzyć to im id'ku za pomocą repo (ja raczej korzystam w entityManagera) do encji do której chcesz się dostać. Wtedy @Transactional w teście nie będzie potrzebny.

0

@Black007 adnotacja @OneToOne ma domyślnie ustawione FetchType.EAGER.

@Black007 @MateInf chodzi o to żeby wgl. usunąć relację OneToOne i trzymać tylko id obiektu?

2
Scarilt napisał(a):

@Black007 @MateInf chodzi o to żeby wgl. usunąć relację OneToOne i trzymać tylko id obiektu?

to miałem na myśli

0

@MateInf mam jeszcze jeden problem - odkąd zacząłem dodawać dane testowe przez plik sql to nie przechodzą mi testy, które zapisują coś w bazie. W tym pliku dodaję np. 15 użytkowników z id 1, 2, 3 itd., z góry określam ich id, bo muszę wiedzieć w testach co wyciągnąć i później w teście, który ma utworzyć nowego użytkownika dostaję błąd związany z naruszeniem ograniczenia klucza głównego lub indeksu unikalnego. W każdej encji mam adnotację @GeneratedValue(strategy = GenerationType.IDENTITY). Wcześniej dodawałem dane testowe przez repozytoria Spring Data JPA i było dobrze. W czym tu jest problem?

0
Scarilt napisał(a):

@MateInf mam jeszcze jeden problem - odkąd zacząłem dodawać dane testowe przez plik sql to nie przechodzą mi testy, które zapisują coś w bazie. W tym pliku dodaję np. 15 użytkowników z id 1, 2, 3 itd., z góry określam ich id, bo muszę wiedzieć w testach co wyciągnąć i później w teście, który ma utworzyć nowego użytkownika dostaję błąd związany z naruszeniem ograniczenia klucza głównego lub indeksu unikalnego. W każdej encji mam adnotację @GeneratedValue(strategy = GenerationType.IDENTITY). Wcześniej dodawałem dane testowe przez repozytoria Spring Data JPA i było dobrze. W czym tu jest problem?

Nie wiem, stackOverFlow albo chat gpt pewnie wie. Natomiast, po co ustawiasz z góry idiki? Logicznie myśląc, ze wszystkich pól które może jakkolwiek być najbardziej "nadzorowane" przez omry (pewnie hibernate) - IDki są zdecydowanie tymi polami. Po co więc ingerować swoimi działaniami jak się nie zna świetnie bebechów co się tam dzieje. Nie możesz np. ustawić sobie unikatowe nazwy userów i w teście po tych userach wyciągać rekordy z bazy do testu ?

0

Tak to jest jak metody serwisowe zwracają encje JPA zamiast zmapowane DTOsy :P

1

Jak tak oftopujemy to:

Tak to jest jak metody serwisowe zwracają encje JPA zamiast zmapowane DTOsy 😛

DTO to raczej warstwa prezentacji (np. Rest Controller), serwisy w ujęciu "springowym" mają za zadanie właśnie zarządzać sesją — Black007 dziś, 07:09

Nope. Owszem zarządzać sesją, ale jak zwracają encje JPA zamiast zmapowane DTOsy, to się kończy jak w niniejszym wątku. — Pinek 1 minuta temu

Pracuję już 12 lat i widziałem chyba wszystkie możliwe rozwiązania

  1. encja na twarz i pchasz
  2. encje w serwisach,w kontrolerach dto
  3. encje nie wychodzą z repo
  4. w ogóle nie używamy hibernate/JPA

Przyznam szczerze iż im wyższy liczba tym bardziej podoba mi sie rozwiązanie

2
Pinek napisał(a):

Tak to jest jak metody serwisowe zwracają encje JPA zamiast zmapowane DTOsy :P

DTOsy rozwiązują część problemów i generują zupełnie nowe, jeszcze bardziej porypane:
mappery, detached entities.
Szczególnie zapis jakiegoś złożonego obiektu do bazy bywa zabawny. W jednym korpo architekci wymyślili, że będą różne DTOsy (ETO, DTO,CTO :-)) - każdy na kolejnym poziomie. Z samych maperów można było zrobić doktorat.

0
jarekr000000 napisał(a):
Pinek napisał(a):

Tak to jest jak metody serwisowe zwracają encje JPA zamiast zmapowane DTOsy :P

DTOsy rozwiązują część problemów i generują zupełnie nowe, jeszcze bardziej porypane:
mappery, detached entities.
Szczególnie zapis jakiegoś złożonego obiektu do bazy bywa zabawny. W jednym korpo architekci wymyślili, że będą różne DTOsy (ETO, DTO,CTO :-)) - każdy na kolejnym poziomie. Z samych maperów można było zrobić doktorat.

No nie wiem - w obecnym projekcie serwisy zwracają DTOsy i czuję się w tym o wiele lepiej niż w wielu poprzednich projektach, gdzie zwracane były encje JPA.

0

@MateInf użytkownicy mają unikatowe nazwy, tzn. email jest unikatowy, ale mam endpointy i metody w serwisach, które wyciągają użytkownika po id i żeby napisać test dla takiej metody muszę wiedzieć jakie id ma konkretny użytkownik.

0

Jak już wróciliśmy z krainy opfftopa...
Mam 18 lat doświadczenia 😛
I widzę że:
@Scarilt warstwy Ci wyciekają. Ten test nie może nie być Transactional, bo jawnie używasz repository!
Pewnie w teście potrzebujesz wartości, która jest zwracana przez to repo.
Zamiast repo z wywołaną metodą wrzuć tą wartość (która jest zwracana przez repo) i wywal Transactional.

0
@Test
void shouldReturnEventToEdit() {
    // when
    var givenAppUser = new AppUser();
    var eventToEdit = organizerEventService.findEventToEdit(givenAppUser, 5L);
    // then
    assertThat(eventToEdit.getName()).isEqualTo("Java Dev Talks #5");
    assertThat(eventToEdit.getEventImageData()).isNotBlank();
    assertThat(eventToEdit.getEventImage()).isNull();
    assertThat(eventToEdit.getEventType()).isEqualTo(MEETING);
    assertThat(eventToEdit.getDateTime()).isEqualTo(TOMORROW.plusMonths(1).format(DateTimeFormatter.ISO_LOCAL_DATE_TIME));
    assertThat(eventToEdit.getLanguage()).isEqualTo("polski");
    assertThat(eventToEdit.getAdmission()).isEqualTo(FREE);
    assertThat(eventToEdit.getCity()).isEqualTo("Rzeszów");
    assertThat(eventToEdit.getLocation()).isEqualTo("WSIiZ");
    assertThat(eventToEdit.getAddress()).isEqualTo("Sucharskiego 2, 35-225 Rzeszów");
    assertThat(eventToEdit.getDescription()).isEqualTo("Spotkanie rzeszowskiej grupy pasjonatów języka Java.");
}

Coś w ten deseń

1

@Black007 jeśli chodzi o ten problem z pierwszego posta to mam jakieś tipy jak to naprawić, ale na razie muszę poprawnie dodać dane testowe. Pisałem wyżej o tym.

0

@Majksu
Chodzi o to, że zwykle w Springu masz tak: JpaRepo jest używane (czyli wstrzyknięte) przez service. Service ma jakąś metodę, która używa tego repo, np. dostałem na wejściu imię i nazwisko i chce to zapisać, Robię sobie new Person i ustawiam tam te dane i zapisuje. Ta metoda jest oznaczona @transactional, czyli aop springa tworzy transakcje, a po wyjściu ją commituje (skrót myślowy tutaj). I teraz mam "Klineta" tego serwisu, np. Fasadę lub controller restowy. I ta fasada, czy controller nic nie wiedzą o czymś takim jak sesja bazodanowa, to też same nie muszą mieć transactional.
Teraz pytanie, czy majac taki podział, to czy chcę testować zapis do bazy? Powinien testować funkcjonalność, a test czy zapisuje się do bazy, tak naprawdę testuje czy baza danych działa.

0

@Black007 jaką funkcjonalność w ten sposób przetestujesz? Ja myślę, że dla kontrolerów powinniśmy pisać testy integracyjne (tutaj różne osoby używają różnych nazw na ten typ testów), w których np. za pomocą RestTemplate strzelisz na endpoint i przelecisz od kontrolera do samej bazy. A Ty chciałbyś mockować odpowiedzi z metod repozytoriów, czy nawet z metod serwisów? W kontrolerach chyba nie masz skomplikowanej logiki, tylko raczej wywołanie jakiejś metody z serwisu i ewentualnie opakowanie zwróconego obiektu w obiekt typu ResponseEntity.

1

W kontrolerach chyba nie masz skomplikowanej logiki, tylko raczej wywołanie jakiejś metody z serwisu i ewentualnie opakowanie zwróconego obiektu w obiekt typu ResponseEntity.

Dokładnie tak. Controlery tylko wywołują fasade.
Dlatego testy integracyjne mogą być na poziomie fasady. Czyli tego co ma się tam dziać, logiki biznesowej.
Można użyć testów czarnej skrzynki.
Wrzucasz parametry i sprawdzasz oczekiwaną wartość.
Albo białej skrzynki. Mockujesz np. Repository i sprawdzasz czy dany mock był wywołany np. 1 raz z danymi parametrami.

Zarejestruj się i dołącz do największej społeczności programistów w Polsce.

Otrzymaj wsparcie, dziel się wiedzą i rozwijaj swoje umiejętności z najlepszymi.