Podmiana implementacji typu implementującego interfejs

0

Mamy standardowy serwis i repozytorium

@Service
class TestService {
    private final TestRepository testRepository;

    @Autowired
    TestService(TestRepository testRepository) {
        this.testRepository = testRepository;
    }
    //...
}

@Repository
interface TestRepository extends CrudRepository<TestEntity, Long> {
}

@Entity
class TestEntity {
    @Id
    private Long id;
    //...
}

Chcę teraz podmienić implementację TestRepository na własną (np. na potrzeby testów). Mam coś takiego:

class InMemoryCrudRepository<T, ID> implements CrudRepository<T, ID> {

    private final Map<ID, T> repository = new HashMap<>();

    @Override
    public <S extends T> S save(S s) {
        return s;
    }
    //...
}

Chodzi o to, żeby "podstawić" InMemoryCrudRepository pod TestRepository.

class TestServiceTest {

    private final TestService testService;

    TestServiceTest() {
        testService = new TestService(new InMemoryCrudRepository<TestEntity, Long>()); //to się oczywiście nie skompiluje
    }
}

Do konstruktora muszę przekazać coś, co:

  1. Pochodzi od TestRepository
  2. Rozszerza InMemoryCrudRepository
    Mój pomysł:
class InMemoryTestRepository<T, ID> extends InMemoryCrudRepository<T, ID> implements TestRepository {
}

Niestety dostaję błąd:

Error:(3, 1) java: org.springframework.data.repository.CrudRepository cannot be inherited with different arguments: <com.example.demo.TestEntity,java.lang.Long> and <T,ID>

Rozwiązaniem jest sparametryzowanie TestRepository

@Repository
interface TestRepository<X, Y> extends CrudRepository<X, Y> {
}

ale wtedy w serwisie musiałbym podać typy, żeby dało się tego normalnie używać.

@Service
class TestService {
    private final TestRepository<TestEntity, Long> testRepository;
    //było private final TestRepository testRepository;
}

A tego bym nie chciał. Jest jakiś inny sposób, żeby w serwisie podmienić implementację TestRepository? (o ile to możliwe)

1

1, Tworzyc konfiguracje recznie
2. Zmusic springa zeby uzyl odpowiedniego beana dla odpowiedniego profilu lub jego braku
3. Order / Primary + definicja beana testowego w test scope

1

Ale dlaczego to InMemoryRepository ma generyki? Zrób dedykowane i będzie w porządku, chyba że tworzysz jakiś framework :)

0

Jesteś pewien, że potrzebujesz tej implementacji InMemory? Rozumiem, że to ma być jakaś implementacja do testów odcięta od bazy, nie lepiej by Ci było w testach podpinać się do jakiejś faktycznej bazy in memory (np. H2) albo setupować sobie test containers?

0

@artur52
Mógłbyś trochę bardziej objaśnić (kompletnie nie czaję :P)

@Charles_Ray
Dlatego, żeby InMemoryRepository mogło być użyte do zastąpienia wielu różnych repozytoriów.

@superdurszlak

Jesteś pewien, że potrzebujesz tej implementacji InMemory? Rozumiem, że to ma być jakaś implementacja do testów odcięta od bazy

Tak. Chodzi o to, żeby nie stawiać kontekstu Springa (chociaż mi to jakoś bardzo nie przeszkadza), więc H2 i testcontainers odpadają. (BTW testcontainers to genialna sprawa, chociaż u mnie na kompie za cholerę nie może się połączyć z Dockerem :|)

Chciałbym jakoś zastąpić Mockito czymś "normalnym", ale rozwiązanie musi być maksymalnie proste, żeby zwiększyć szanse jego przeforsowania.

2
Potat0x napisał(a):

Tak. Chodzi o to, żeby nie stawiać kontekstu Springa (chociaż mi to jakoś bardzo nie przeszkadza), więc H2 i testcontainers odpadają. (BTW testcontainers to genialna sprawa, chociaż u mnie na kompie za cholerę nie może się połączyć z Dockerem :|)

Chciałbym jakoś zastąpić Mockito czymś "normalnym", ale rozwiązanie musi być maksymalnie proste, żeby zwiększyć szanse jego przeforsowania.

A, ok, czyli po prostu chcesz poskładać sobie w kupę te komponenty bez Springa i wstrzyknąć zachowanie żeby ominąć Mockito-izację :D

No to w sumie możesz

  • złożyć ręką (chyba nie masz aż tylu dependencji żeby to był jakiś wielki problem, sądząc po PoC)
  • złożyć jakimś innym toolem do dependency injection żeby nie używać Springa - tylko po co robić sobie pod górkę, wg. mnie byłoby bez sensu
  • skonfigurować testy tak, żeby nie ładowały całego kontekstu aplikacji a jedynie potrzebne komponenty

A co do tej testowej implementacji, klasa implementująca i tak nie będzie generyczna, skoro implementuje skonkretyzowany interfejs TestRepository, takie coś Cię nie urządza?

class InMemoryTestRepository extends InMemoryCrudRepository<TestEntity, Long> implements TestRepository {
}
0

Dokładnie o to chodziło, mój mózg chyba nie pracuje :D
złożyć ręką (chyba nie masz aż tylu dependencji żeby to był jakiś wielki problem, sądząc po PoC) - w sumie to teraz nie pamiętam, ale myślę że kilka przypadków da ze zdemockitować, albo przynajmniej inaczej pisać nowe testy. W używaniu Mockito męczy mnie, że

  • muszę analizować bebechy testowanych klas żeby sprawdzić jakich funkcji z repozytoriów używają (to jest chore)
  • zazwyczaj jest testowana tylko część czytająca (co przy okazji zaniża pokrycie kodu)
  • nie da się sprawdzić, co zostało zapisane do repozytorium podczas testów (to jest moja główna motywacja)

Naprawdę nie dziwię się, że ludziom nie chce się pisać testów, jeżeli tak to wygląda.

Osobiście użyłbym po prostu Testcontainers, bez rozwodzenia się nad podziałem na testy jednostkowe/integracyjne/blabla, ale jak to zazwyczaj wygląda w przypadku serwerów CI? Zdarzają się problemy?

nie będę nawet próbował bronić podejścia z H2, pałam do H2 szczerą niechęcią

Masz jakieś "ciekawe" doświadczenia z tym związane?

1

@Potat0x:

  1. spójrz na to: LINK:
  2. @profile i profil startowy aplikacji.
  3. Przykryj pustym interfejsem. W pakietach testowych zrób sobie impl. tej klasy i wrzuć @Primary
2
Potat0x napisał(a):
  • nie da się sprawdzić, co zostało zapisane do repozytorium podczas testów (to jest moja główna motywacja)

Zależy co rozumiesz przez co zostało zapisane do repozytorium - to co zostało wrzucone do repozytorium jako entitka czy to co repo robi z tym pod spodem? Jak to drugie no to taką in-memory implementacją i tak tego nie sprawdzisz, bo zastępujesz to swoją atrapą :P

Jeśli chcesz podejrzeć, co się działo z repo, to jak najbardziej możesz, Mockito nie kończy się na mock i when. Do tego są jeszcze poboczne paczki, część w samym Mockito, część bodajże z hamcrest, samego JUnit i co to tam jeszcze jest. Wybacz, ale nie pamiętam w tej chwili co jest skąd, nie mam do tego głowy

  • spy - zamiast pustej wydmuszki dostajesz w pełni funkcjonalny obiekt, na którym dodatkowo możesz dokładać np. jakieś weryfikacje i takie tam. Jak nazwa wskazuje, podglądać go ;)
  • ArgumentMatchers - do sprawdzania, czy argumenty wywołań pasują do jakichś kryteriów, mniej lub bardziej restrykcyjnych
  • verify - do sprawdzania wywołań (można też robić asercje, że coś jest wywołane maksymalnie 2 razy albo wcale itd)
  • ArgumentCaptors czy jakoś tak - możesz przechwycić argumenty wywołań, wyciągnąć je i coś tam z nimi zrobić
  • InOrder - podobnie jak verify, ale dodatkowo możesz np. weryfikować kolejność wywołań

Naprawdę nie dziwię się, że ludziom nie chce się pisać testów, jeżeli tak to wygląda.

Osobiście użyłbym po prostu Testcontainers, bez rozwodzenia się nad podziałem na testy jednostkowe/integracyjne/blabla, ale jak to zazwyczaj wygląda w przypadku serwerów CI? Zdarzają się problemy?

Musiałbym zapytać kolegów z zespołu obok, jakie mieli doświadczenia z test containers, sam jestem skazany na używanie H2. Generalnie idea brzmi dużo rozsądniej od podkładania 'atrapy' bazy. Na dobrą sprawę brzmi też lepiej od robienia takiego własnego 'in memory', bo testując bardziej integracyjnie / e2e możesz chcieć uwzględnić (a możesz nawet nie wiedzieć, że chciałbyś) interakcje z prawdziwą bazą, szczególnie w jakichś bardziej rozbudowanych scenariuszach. Na przykład możesz mieć nie jeden data source, a kilka (i np. routować do jakichś read-only DS dla niektórych transakcji), a do czegoś takiego musiałbyś już stawiać taką atrapę, że operacja Fortitude wymięka ;)

nie będę nawet próbował bronić podejścia z H2, pałam do H2 szczerą niechęcią

Masz jakieś "ciekawe" doświadczenia z tym związane?

Cały szereg drobnych acz nieszczęśliwych doświadczeń, przede wszystkim z native queries i wykorzystywaniem ficzerów danego DBMS*. I występowaniem różnych drobnych rozbieżności między stanem rzeczywistym, a tym, który H2 uznaje za rzeczywisty i 'kompatybilny'.

* inb4 "nie używaj native queries" - czasami potrzebujesz czegoś, co ma SQL danego DBMS a JPQL / HQL już nie, czasami używasz czegoś innego niż Hibernate albo ma jakiś własny customowy DSL / QL, albo używa native queries

0

Gdy bawiłem się w architekture heksagonalną to robiłem coś takiego

@Configuration
public class MyConfiguration {

    @Bean
    MyService myService(MyRepository myRepository) {
        return new MyService(myRepository);
    }

    MyService myService() {
        return new MyService(new InMemoryMyRepository());
    }
}

Serwis zostawał bez adnotacji @Service, gdy trzeba było go wstrzyknąć wykorzysytwany była metoda z adnotacją @Bean, gdy potrzebowałem go dla testów to tworzyłem go ręcznie przez drugą metodę.

Innym sposobem jest też używanie @ActiveProfile przy testach i dodawanie odpowiednich @profile do repozytoriów.

0

W Springu jest jeszcze taka fajna adnotacja jak @MappedSuperClass, poczytaj sobie o niej

1
piotrek2137 napisał(a):

W Springu jest jeszcze taka fajna adnotacja jak @MappedSuperClass, poczytaj sobie o niej

Jeśli mówimy o tej samej adnotacji, to nie MappedSuperClass tylko MappedSuperclass, nie w Springu a w JPA, i nie rozwiąże problemu OPa bo służy do czegoś zupełnie innego :D

0

Przypominam, że chodzi o możliwość odpalenia testów bez udziału Springa ;)

Zależy co rozumiesz przez co zostało zapisane do repozytorium - to co zostało wrzucone do repozytorium jako entitka czy to co repo robi z tym pod spodem? Jak to drugie no to taką in-memory implementacją i tak tego nie sprawdzisz, bo zastępujesz to swoją atrapą :P

To pierwsze.

Jeśli chcesz podejrzeć, co się działo z repo, to jak najbardziej możesz, Mockito nie kończy się na mock i when. Do tego są jeszcze poboczne paczki, część w samym Mockito, część bodajże z hamcrest, samego JUnit i co to tam jeszcze jest. Wybacz, ale nie pamiętam w tej chwili co jest skąd, nie mam do tego głowy

Problem w tym, że nie chcę używać Mockito żeby sprawdzić, co się działo. Wolałbym przeprowadzać testy takim sposobem: wpuszczam dane do systemu jedną funkcją -> sprawdzam co zwróciła ta funkcja i/lub co zwracają inne, które czytają z repozytorium. Całkowicie bezproblemowe testowanie. Wewnątrz serwisu repozytorium powinno paiętać zapisane w nim encje.

w pełni funkcjonalny obiekt

Da się za pomocą Mockito zrobić repozytorium, które zachowuje się jak normalne?

        testRepository.save(new TestEntity(123L, "test")); //zwraca zapisany obiekt
        testRepository.findById(123L); //zwraca zapisany obiekt
2

Możesz zrobić Mockito.spy(), ale sama dokumentacja nawet mówi, że to jest coś niepożądanego.

1

@Charles_Ray: dokumentacja mówi, że niepożądane są partial mocki, a nie spy object sam w sobie. Zresztą, zrobienie mocka z kaskadą thenCallRealMethod() przeplatanego z thenAnswer / thenReturn / thenThrow byłoby w sumie jeszcze gorsze... ;)

W każdym razie, w momencie, gdy spy object niczego nie mockuje, a jedynie pozwala prześledzić użycie - to właściwie nie ma tu żadnego partial mocka w tym sensie, że test double zachowywałby się częściowo jak prawdziwy obiekt, a częściowo jak wydmuszka. To jest chyba intencja @Potat0x sądząc np. po tym zdaniu:

nie da się sprawdzić, co zostało zapisane do repozytorium podczas testów (to jest moja główna motywacja)

No i w tym momencie można by zrobić minę Smerfa Ważniaka i oświadczyć

jak musisz testować implementację to znaczy, że źle napisałeś kod, w testach to powinien być black box, sugeruję przepisanie wszystkiego od nowa i wtedy nie będziesz tego potrzebował

Ale to chyba nie byłoby zbyt pomocne, gość próbuje wskórać cokolwiek i wątpię, że koledzy by posłuchali jakby wyskoczył z propozycją "słuchajcie, mam super patent na testowanie, same dobre praktyki, musimy jedynie poświęcić trzy miesiące na przepisywanko tego klocucha i będziemy mogli zasuwać z tematem" :D

5

Ja mam ogólną alergię na mocki, bo bardzo łatwo stracić kontrolę nad tym co właściwie się testuje. Kończy się to tym, że testujemy w dużej mierze mocki zamiast prawdziwej logiki. Zamiast mockować można napisać tego InMemoryRepository. Jest koszt związany z napisaniem takiej klasy, ale za to jest zysk z tego, że nie trzeba mockować. InMemoryRepository można testować dokladnie tymi samymi testami co H2Repository czy OracleRepository, a to zmniejsza koszt utrzymania InMemoryRepository - skoro mamy testy automatyczne to wykrywają one błędy w InMemoryRepository od razu. Zamockowane wywołania repozytorium natomiast przeterminowują się mniej więcej tak samo szybko jak dokumentacja do metod (jeśli zmienia się kontrakt metody to trzeba poprawić zarówno dokumentację jak i jej zamockowane wywołania) - dziwne, że to szybkie przeterminowanie prowadzi do zaniechania pisania dokumentacji, ale do zaniechania mockowania już nie.

Mockując repozytorium i weryfikując operacje na nim nie tylko sprawdzamy co do niego wchodzi, ale też w jaki sposób i w jakiej kolejności. To zbyt dużo jeśli repozytorium nie jest klasą wprost testowaną, a tylko pomocniczą. Jest tak gdy testujemy na przykład obiekty domenowe czy endpointy (jeśli mam bazkę in memory opartą o kolekcje to czemu nie robić takich kompleksowych testów endpointów? dalej są szybkie, a testują sporą część logiki). Jeśli weryfikujesz zbyt dużo w mockach to testy stają się bardzo sztywne, rozwijanie logiki biznesowej wymaga poprawy wielu testów, które nawet nie testują wprost nowej funkcjonalności tylko jakąś inną. Jeśli natomiast zgodnie z sugestiami z dokumentacji Mockito olewasz weryfikację to moim zdaniem testy trochę tracą sens. Weźmy na przykład scenariusz:

  1. dodaj przedmiot
  2. zmień atrybut przedmiotu
  3. wyciągnij aktualną postać przedmiotu
  4. sprawdź jego zawartość

Kroki 1 i 2 nie zwracają danych, więc nie trzeba ich mockować by test przeszedł. Weryfikację jak już napisałem olaliśmy, więc w zasadzie te kroki 1 i 2 odpadają. Zostają kroki 3 i 4, ale teraz już postać testu się zmienia - zamiast testować czy obiekty domenowe radzą sobie z zapisem, modyfikacją i odczytem z repozytorium testujemy tylko czy radzą sobie z odczytem. Takich dylematów z mockowaniem jest sporo. Często je mam jak widzę kod usiany mockami. Ciągle jest pytanie: co mam mockować i ile nawstawiać weryfikacji żeby test miał sens?

Co do porównania InMemoryRepository (w sensie oparte o kolekcje) kontra H2Repository (w sensie oparte o bazę danych w pamięci) to przerabiałem ten temat. Tworzenie kilku pustych kolekcji w nowym InMemoryRepository jest o rzędy wielkości szybsze niż stawianie nowej bazki H2 w pamięci i tworzenie w niej kilku tabel. Można temu przeciwdziałać tworząc pulę baz H2 w pamięci, ale jeśli chcemy mieć pełną izolację testów to trzeba się nakombinować by to uzyskać (sprawdzać które tabele już są utworzone w bazce, jeśli inne niż potrzebne w teście to coś grzebiemy, itd). Jeśli się da to jak najbardziej trzeba izolować testy - testy które działają odpalone pojedynczo, ale wywalające się podczas odpalenia grupy testów są zdecydowanie kiepskie.

InMemoryRepository ma też tę zaletę, że można od tego w ogóle zacząć prototypowanie aplikacji. Zamiast zastanawiać się jak mają wyglądać docelowe tabele w bazie danych, optymalizować SQLe zawczasu itd można jechać na InMemoryRepository i np dorzucić jakąś prymitywną serializację całej bazki do JSONa. Dzięki tej serializacji dane przeżywają restart aplikacji, a zamiast od razu poświęcać czas na zajmowanie się bazą danych można klepać nowe funkcjonalności.

0

nie da się sprawdzić, co zostało zapisane do repozytorium podczas testów (to jest moja główna motywacja)

No i w tym momencie można by zrobić minę Smerfa Ważniaka i oświadczyć

jak musisz testować implementację to znaczy, że źle napisałeś kod, w testach to powinien być black box, sugeruję przepisanie wszystkiego od nowa i wtedy nie będziesz tego potrzebował

A co mam testować, jak nie implementację? :D Miałeś na myśli to, że nie powinno się sprawdzać bezpośrednio w repozytorium co się stało? Zgadzam się, to "sprawdzenie" ma się odbywać przez logikę serwisową. Tyle, że wtedy potrzebne jest repozytorium które zachowuje się normalnie, a nie mock. Trochę nieprecyzyjnie to napisałem, chodzi mi to to:

Problem w tym, że nie chcę używać Mockito żeby sprawdzić, co się działo. Wolałbym przeprowadzać testy takim sposobem: wpuszczam dane do systemu jedną funkcją -> sprawdzam co zwróciła ta funkcja i/lub co zwracają inne, które czytają z repozytorium.

To lepiej odwzorowuje zachowanie aplikacji, niż mockowanie, szpiegowanie, itp.

"słuchajcie, mam super patent na testowanie, same dobre praktyki, musimy jedynie poświęcić trzy miesiące na przepisywanko tego klocucha i będziemy mogli zasuwać z tematem"

Jakie przepisywanie? Wystarczy inaczej pisać nowe testy :)

Gdzie widzę sens mockowania:

  • zewnętrzna zależność
  • trzeba skorzystać z czegoś, co jeszcze nie istnieje
  • jakaś cieżka operacja (np. obliczenia trwające kilkanaście sekund i zwracające true/false) - zamiast czekać można zrobić prostego mocka dla dwóch przypkadów, a obliczenia przetestować osobno

Jakiś czas temu też chciałem sobie coś zamockować, założyłem tu temat. Skończyło się na tym, że nie mam mocków. Testy idą trochę długo (mielenie Dockerem), ale jeżeli przejdą to, to szansa na ukrytego buga jest naprawdę mała.

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