Spring Boot Test problem z konfiguracją Contextu

0

Cześć,

mam nietypowy problem.
Mam testy w SpringBoocie. Jak odpalam tylko tą jedną klasę to wszystko działa.
Natomiast, jak odpalę wszystkie na raz, to ta klasa failuje.
Są to testy integracyjne - test opiera się w głównej mierze do zapisu do bazy i odczytu z niej rekordu.
W konsoli widzę bezpośrednio insert, z poziomu kodu nawet specjalnie wypisuje sobie jej ID, ale select nie zwraca nic. I mam mocne WTF
Select robię za pomocą JPA Repository:

@Autowired
KlasaRepository klasaRepository;

W klasie testowej

Spotkał się ktoś z czymś takim, nie do końca wiem, gdzie i co szukać.

Klasa wpięta z:

@AutoConfigureTestDtabase
@SpringBootTest
@DirtiesContext
@ExtendWith(SpringExtension.class)

Za nakierowanie, pomoc z góry dzięki

0

klasa failuje

jaki dokładnie błąd leci?

I odpalasz pojedynczy i wszystkie task samo (czyli np oba poprzez IJ lub oba poprzez terminal)?

0

@Pinek

Znaczy błąd leci taki, że w tej klasie, test opiera się na wywoływaniu metody, która zapisuje do bazy danych.
Natomiast po tym jest wykonywana metoda z repozytorium.
findByID

I tutaj jest fail, ponieważ nie znajduje tego rekordu. Choć tak jak wspomniałem w konsoli widzę, że jest zapis, wypisuje sobie specjalnie nawet ID.
Co jest najdziwniejsze jak odpalę tylko tą klasę to działa, ale jak odpalam wszystkie klasy testowe, to już nie działa. Nie znajduje wspomnianego ID

Za każdym razem odpalam z poziomu IDE z takimi samymi parametrami.

0

Pokaż ten test

1

jak odpalam wszystkie klasy testowe, to już nie działa. - rozumiem, że odpalasz z poziomu IDE wszystkie testy projektu. Sekwencyjnie.

A czy jak wywołujesz findByID to ta transakcja ma prawo widzieć zmiany? (Może jest otwarta wcześniej?).

1

A daj @Transactional na klasę testowa, powinno zadziałać

0

Poniżej przesyłam kod, jest on obkrojony, ale generalnie nie różni się.(Niestety nie mogę tu wrzucić całego projektu, choć tak by było prościej)

@Entity
@Getter
@Setter
@Builder
@AllArgsConstructor
@NoArgsConstructor
public class SimpleEntity {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    private Long longColumn;
    private String firstColumn;
}
@Data
@AllArgsConstructor
public class SimpleEntityDto {

    private Long longColumn;
    private String firstColumn;
}
@Repository
public interface SimpleEntityRepository extends JpaRepository<SimpleEntity, Long> {
}
@Service
@RequiredArgsConstructor
public class SimpleEntityService {

    private final SimpleEntityRepository simpleEntityRepository;

    public void saveSimpleEntity(SimpleEntityDto simpleEntityDto) {

        SimpleEntity simpleEntity = SimpleEntity.builder()
                .firstColumn(simpleEntityDto.getFirstColumn())
                .longColumn(simpleEntityDto.getLongColumn())
                .build();

        SimpleEntity savedSimpleEntity = simpleEntityRepository.saveAndFlush(simpleEntity);
    }
}
@ExtendWith(SpringExtension.class)
@AutoConfigureTestDatabase
@SpringBootTest
@Transactional
public class SimpleEntityServiceTest {

    @Autowired
    SimpleEntityService simpleEntityService;

    @Autowired
    SimpleEntityRepository simpleEntityRepository;

    @Test
    public void shouldSaveSimpleEntity() throws Exception {

        SimpleEntityDto simpleEntityDto = new SimpleEntityDto(99L, "string column");

        simpleEntityService.saveSimpleEntity(simpleEntityDto);

        SimpleEntity simpleEntity = simpleEntityRepository.findById(1L).orElseThrow(() -> new Exception("Did not find Simple Entity"));

        assertEquals(1L, simpleEntity.getId());
        assertEquals(99L, simpleEntity.getLongColumn());
        assertEquals("string column", simpleEntity.getFirstColumn());

    }
}

Mam nadzieje, że coś pomoże, choć pewnie będzie ciężko, bądź niemożliwe mając na uwadze to, że Test failuje tylko, gdy się uruchomi wszystkie raz
Bo tak jak wspomniałem zawilej logiki w nim nie ma.

3

simpleEntityRepository.findById(1L).

A skąd wiesz, że 1 ???

1
  SimpleEntityDto simpleEntityDto = new SimpleEntityDto(99L, "string column");

  simpleEntityService.saveSimpleEntity(simpleEntityDto);

  SimpleEntity simpleEntity = simpleEntityRepository.findById(1L).orElseThrow(() -> new Exception("Did not find Simple Entity"));

  assertEquals(1L, simpleEntity.getId());
  assertEquals(99L, simpleEntity.getLongColumn());

A skąd ta jedynka ma się wziąć w bazie? To co zapisujesz to ID=99L. , więc powinno się wywalić przy wyszukiwaniu na 1 i później na asercji. Chyba, ze coś mi umknęło :)

1

Postaw breakpointa po zapisie > evaluate expression i pobierz wszystko z bazy, zobacz czy cokolwiek tam jest. Stawiasz jakiś kontener do testów czy inmemory db?

1

Nie podoba mi się pomysł @Transactional nad klasą z testami, bo już nie raz widiziałem przypadki że encja w testach nie dotykała nawet bazy, bo kolejny find z repo korzystał dalej z cache JPA:

@Transactional
public class SimpleEntityServiceTest {
2

Nie mam dalszych pomysłów.
IMO magia springa - jak zwykle - jednak nie działa. Czyli, popełniłeś gdzieś drobny błąd i teraz jego szukanie to kwestia szczęścia (i doświadczenia)
Masz dwa wyjścia:

  1. Najfajniej jakby udało Ci się wykroić minimalny "powtarzalny" przykład i wrzucić na GH - chyba nadal lubie ( i nie tylko ja) takie gówna śledzić.
  2. Jak to w pracy - to możesz zrobić Na niemieckiego architekta: oznacz test jako @Ignore i udawaj, że to nie twój problem. Wyjebie się na produkcji, ale to spring, więc i tak by się wyjebało :-)
2

Możliwe, że dla tej bazy in-memory strategia strategy = GenerationType.IDENTITY działa w oparciu o sekwencję, jeśli więc jedziesz testami sekwencyjnie oraz:
Test#1 - robi create SimpleEntity -> ID=1 zostaje zjedzone, sekwencja bazodanowa zostaje podbita do 2, test czyści dane z SimpleEntity (ale sekwencja nadal żyje)
Test#2 - robi createSimpleEntity -> ID z sekwencji 2 -> sekwencja podbita do 3

Możliwe, że gdzieś w testach potrzebujesz jawnego restartu sekwencji (@Before/@After)
ALTER SEQUENCE DUPA RESTART WITH 1;

Przy testach uruchamianych sekwencyjnie, nie wyjaśnia to jednak braku wpisów dla findAll. W testach uruchamianych równolegle byłaby szansa na race condition.

Jaka to konkretnie baza in-memory? Czy masz pulę wątków połączeń?

Inne wyjaśnienie dla tego findAlla, to właśnie pula połączeń i np. wstawiasz rekordy w oparciu o connection#1, findAlla robisz na connection#2 (wówczas zmiana rozmiaru puli połączeń bazodanowych na 1 byłaby pewnego rodzaju obejściem problemu).

Debugger prawdę Ci powie ;-)

1

Skoro H2 ... to H2 ma taki jeden śmieszny ficzur, który czasem potrafi doprowadzić do łez.
Poczytaj o ;DB_CLOSE_DELAY=-1

Nie wiem czy to jest problemem, ale jest jakaś szansa.

0

Luźno przyglądam się tematowi i nie widząc całości kodu nie chcę zgadywać co może być nie tak. Natomiast IntelliJ ma taką fajną opcję, jak puszczanie testów nonstop aż do faila. Bardzo często okazuje się to pomocne.

0

Ja mam taki pomysł.
1.Skonfiguruj bazę H2 w pliku properties.
2.Niech metoda do zapisywania encji zwraca idka tej zapisanej encji.
3. W teście użyj tylko adnotacji @SpringBootTest (bazę po teście możesz wyczyścić jakimś after eachem z metodą deleteAll(), @Transactional lepiej nie używać jak nie ma takiej potrzeby.)
4.W teście wstrzyknij tylko serwis. Nie potrzebujesz tego repo, bo ten serwis zależy od niego. Musisz tylko dodać do serwisu metodę findById.
5.I teraz test będzie wygladał tak, że tworzysz sobie to dto, przekazujesz do metody save z serwisu, ta metoda zwraca idka zapisanej encji, potem w asercji za pomocą metody findById sprawdzasz czy istnieje taka encja i czy ma odpowiednie pola i już.

2

A jak w tym teście dasz na chama repo.findAll() i wyprintujesz wszystkie znalezione encje, to coś tam w ogóle jest czy nie? Czy baza jest pusta? Przy puszczeniu wszystkich testów oczywiście, bo ten przypadek debugujesz.

Podejrzewam, że tam się generuje jednak inny id niż 1 i jakieś rekordy jednak są.

0

Na wstępie Panowie bardzo dziękuje za aktywność i za chęci podjęcia pomocy. Poniżej wysyłam odpowiedzi + moje subiektywna opinia / to co udało mi się gdzieś tam przetestować

@Charles_Ray:
Właśnie przy wywołwaniu repo.findAll() nic nie zwraca, jest pusto.
Więc to nie jest kwestia wyszukiwania złego ID.

@jarekr000000: ;DB_CLOSE_DELAY=-1 .
Dodałem to, choć nie byłem pewien, czy mi to zadziała z adnotacją @AutoconfigureTestDatabase
Więc w properties ustawiłem bazę H2

Tylko teraz dostaję inny błąd przy odpaleniu wszystkich testów:
Unable to build HibernateSessionFactory; nested exception is org.hibernate.exception.GenericJDBCException: Unable to open JDBC Connection for DDL execution
Caused by: org.h2.jdbc.JdbcSQlException: Function "LOCK_MODE" not found; SQL statement:
CALL LOCK_MODE()

Z adnotacją @AutoconfigureTestDatabase ten problem się nie pojawia

Może to być jakiś trop. Rozumiem sam błąd, aplikacja normalnie korzysta z Oracle, ale tutaj jest wywoływana funkcja, która nie istnieje w H2.
Tylko w jaki sposób mógłbym ją znaleźć w kodzie. Wykorzystuje w projekcie IgniteCache i być może jakoś jego wewnętrzna funkcja to wykorzystuje.
Ale nie mam pomysłu na to

@yarel:
Poszedłem w kierunku puli wątków połączeń. Więc Usunąłem adnotację @AutoconfigureTestDatabase
Zestawiłem w properties dostęp do bazy H2
spring.datasource.hikari.maxium-pool-size=1 test już nie przechodzi
Dostaję błąd, że nie może zestawić połączenia.
HikariPool-1 - Connection is not available, request timed out after 30001ms.

Gdy ustawię
spring.datasource.hikari.maxium-pool-size=2
To już jest na zielono.

I test nie przechodzi nawet, gdy go odpalę pojedynczo. Nie jest w stanie nawiązać połączenia. Czy to jest normalne ? Może tu gdzieś widnieje przyczyna. Co sądzisz? Chyba, że tak sam w sobie Spring pod spodem działa.
Tak jakby były już tworzone dwa połączenia i wtedy by się zgadzało. Tylko jak to potwierdzić / naprawić

0

Może błąd jest w innym teście niż ten który pokazałeś? W sensie że ten co pokazałeś nie przechodzi, ale źródło problemu jest w innym miejscu.

0

Hmm, a jakbyś dorzucił ręcznie entitymanagera i dodał perista przed saveflush? Nie wiem na ile ta adnotacyjna magia przykrywa EMa...

1

Wołam, tych którzy się udzielali.
@Pinek , @Skoq , @jarekr000000 , @scibi_92, @yarel , @Karaczan , @raxigan , @Sampeteq , @Charles_Ray

Po części udało mi się rozwiązać, choć akurat jeszcze niestety nie udało mi się doszukać finalnej przyczyny, ponieważ nadal nie rozumiem w jaki sposób powstaje ten błąd.
Być może ktoś z wyżej wymienionych będzie w stanie wyjaśnić. Jak się sam czegoś dowiem, to napiszę.

Dziś akurat przysiadłem trochę nad tymi testami i zacząłem się bawić. Na wstępie zacząłem migrację do Junit5 , wyłączyłem wszystkie testy z Junit4
Co zmniejszyło ilość testów i pozwoliło mi zauważyć jedną rzecz.

Testy odpalane razem nie przechodzą, gdy są dwie klasy oznaczone adnotacją @AutoConfigureTestDatabase
W pliku properties mam zostawione tylko
spring.jpa.hibernate.ddl-auto=update

Klasy testowe są odpalone za kolejnością, więc po prostu dodałem to drugiej klasy
@DirtiesContext(classMode = DirtiesContext.ClassMode.BEFORE_EACH_TEST_METHOD)

I teraz już wszystko działa. Niestety jeszcze nie udało mi się ustalić dlaczego tak się dzieje, ale jak się dowiem to napiszę tutaj w temacie. Być może komuś się przyda ale już bez wywoływania.

Miłego Dnia :)

3

Jeżeli polegasz na kolejności odpalania testów, to dalej masz problem niestety. Ustal przyczynę.

2

Ja bym na Twoim miejscu pozbył się tych adnotacji tj. @DirtiesContext i @AutoConfigureTestDatabase -> nigdy tego nie używałem ale widzę, że pierwsza niszczy kontekst, który musi być stawiany na nowo (po co?) a druga automagicznie coś konfiguruje pod spodem, lepiej imo ręcznie to zrobić w pliku konfiguracyjnym. W bazowej klasie, która jest rozszerzana przez resztę testów integracyjnych (zakładam, że taką masz) możesz wrzucić kawałek kodu, który będzie czyścił bazę po lub przed każdym testem coś w tym stylu:

@Before
public void cleanup() {
  dupaRepository.deleteAll();
}

Będziesz miał pewność, że jeden test nie sypie się bo inny coś wstawił/zmienił. Masz prosty test, który nie ma prawa się wywalać a obstawiam, że to wina czegoś co robią te adnotacje xD

1 użytkowników online, w tym zalogowanych: 0, gości: 1