[Java] Przetestowanie metody zwracającej void

0

Cześć
Jak przetestować poniższą metodę w klasie UserService:

public class UserService {
...
    public void updateEntity(User user) {
        repositoryDAO.save(user);
    }
}

Używam mockito, junit 5, jpa

1

Oprócz sprawdzenia czy było uderzenie w metodę .save() zbyt wiele nie przetestujesz :/
Mockito.verify(repositoryDAO, times(1)).save(any());

0

OK. A czy nie można stworzyć sobie jakiegoś obiektu User i wykonać

 repositoryDAO.save(user)

i potem

assertEquals("Jan", service.findById(1).getName());
2
biurostron napisał(a):

OK. A czy nie można stworzyć sobie jakiegoś obiektu User i wykonać

 repositoryDAO.save(user)

i potem

assertEquals("Jan", service.findById(1).getName());

To zależy czy robisz test jednostkowy, czy integracyjny ;)

6

Ta metoda nie ma żadnej logiki więc nie ma sensu jej testować. Chyba że chcesz podwyższyć procent pokrycia kodu. To wtedy czasem nawet do geterów i seterów pisze się testy

0

Z reguły uważam że metody pracujące na bazie nie powinny być void. Tym bardziej .save() który przecież już zwraca zapisywaną encję. Dobrą praktyką po zapisie do bazy była by informacja w response entity że encja taka i taka została zapisana i umieszczenie jej w JSON'ie (lub jej częśći - jakieś zmapowane DTO).
Co do otestowania, na pewno mockito.verify() + test .updateEntity() ze zmockowanym repo.
Poza tym, metoda nazywa się update*(), przemyślałeś czy może wystąpić sytuacja z nieistniejącym id ?

@Edit
np coś takiego

@Override
    public ForumThread update(Long id, ForumThread thread) {
        thread.setId(id);
        return forumThreadRepository
                .findById(id)
                .map(x -> thread)
                .map(forumThreadRepository::save)
                .orElseThrow(PropertyNotFoundException::new);
9

Tak czytam sobie kolejne komentarze i oczom nie wierze :) ludzie, naprawdę mockujecie repozytoria i sprawdzacie wywoływanie metod save(), update(), findById() itd?

0

@Charles_Ray: szczerze mówiąc, to tak :D

Otóż w swoich małych projektach wczytuję całą bazę do kolekcji javowych i na nich sobie pracuję. Używam Spring Data i MongoDB, które traktuję jako persistent storage.
Pisząc logikę biznesową zaczynam od testów, tych jednostkowych, szybkich, bez kontekstu springa.

Pewnego dnia, kiedy napisałem sobie logikę i zapis danych, moje testy szły na zielono, wprowadzałem sobie jakieś dane z frontu. Jakież było moje zdziwinie, kiedy okazało się, że po restarcie aplikacji danych nie ma ;)

Tak, zapisywałem dane do kolekcji w serwisie, ale zapomniałem wywołać save na repozytorium. Z tego powodu napisałem sobie dwie abstrakcyjne klasy jak DataProvider i DataService, coś takiego:

abstract class DataProvider<T : DataDomain>(
        private val emptyList: Boolean = false
) : CrudRepository<T, String> {

    private var items: List<T>

    init {
        items = if (emptyList) List.empty() else sampleData()
    }

    open fun sampleData(): List<T> {
        return List.empty()
    }

    override fun <S : T?> save(p0: S): S {
        this.items
                .find { items -> items.id == p0?.id }
                .map { oldItem ->
                    this.items = this.items
                            .remove(oldItem)
                            .append(p0)
                }
                .getOrElse { this.items = this.items.append(p0) }
        return p0
    }

    override fun findAll(): MutableIterable<T> {
        return this.items
    }

//.... pozostale metody

abstract class DataService<T : DataDomain>(
        val repository: CrudRepository<T, String>
) {
    private var items: List<T> = List.ofAll(repository.findAll())

//.... podobne metody jak w DataProvider

W DataService mam podobne metody jak w DataProvider, tylko np. metoda addOrUpdate zapisuje dane zarowno do kolekcji items jak tez robi save na repository. No i to mam przetestowane raz. W ten sposób wiem, że jak wywołuję addOrUpdate w serwisie pochodnym od DataService to save idzie i do kolekcji, i do bazy danych.

I tak wiem, że jak będzie jakiś błąd z serializacją, albo coś ze Spring Data czy samą bazą, to moje testy i tak będą szły na zielono. Do sprawdzenia tego potrzebne są testy integracyjne z całym kontekstem. Mimo to i tak uważam, że takie sprawdzanie czy jest save na repository jest lepsze niż gdyby tego nie było w ogóle.

2
kkojot napisał(a):

I tak wiem, że jak będzie jakiś błąd z serializacją, albo coś ze Spring Data czy samą bazą, to moje testy i tak będą szły na zielono. Do sprawdzenia tego potrzebne są testy integracyjne z całym kontekstem. Mimo to i tak uważam, że takie sprawdzanie czy jest save na repository jest lepsze niż gdyby tego nie było w ogóle.

Takie testy mają ten problem, że zagłębiają się w wewnętrzną logikę funkcji przez co uniemożliwiają w przyszłości prosty refaktor.

Przykładowo, ktoś postanawia wrzucać Mockito gdzie popadnie i sprawdzać czy konkretne metody się wywołały. Potem ten sam ktoś chce zmienić kilka funkcji (różne powody - zmiana biznesowa, bo można to ogarnąć lepiej itd.) i nagle się okazuje, że ma do poprawienia 200 testów. Taki sposób testowania bardziej przeszkadza niż pomaga i prowadzi do systemu w którym nic nie chcesz zmieniać (bo się nie da).

Może Mockito gdzieś ma jakiś sens (nie udało mi się go jeszcze spotkać, poza mega legacy kodem) ale za każdym razem jak się go chce użyć w nowym kodzie to najpierw zastanowiłbym się dlaczego w ogóle istnieje taka potrzeba. Jest duża szansa, że problemem jest kod, który jest napisany tak, że nie da się go prosto otestować, a nie realna potrzeba użycia Mockito. Może metody robią za dużo, może podział odpowiedzialności jest pomieszany pomiędzy klasami, a może została wybrana zbyt mała jednostka testowa, powodów może być wiele.

0

Cieżko mi jest komentować Mockito, bo nigdy z tego nie korzystałem. Ale korzystam ze Spring Data, bo działa to całkiem nieźle i tak mi jest wygodnie. I z racji tego, że to repozytorium wstrzykuje mi Spring, do testów jednostkowych potrzebuję, no cóż.. mocka, imitację działania tego repozytorium. Stąd takie klasy jak DataProvider i DataService, które opisałem powyżej.

I jeśli mam 200 serwisów, które dziedziczą po DataService, to wywołanie metody save czy update w repository mam przetestowane tylko w jednym miejscu - w teście klasy abstrakcyjnej DataService. Dlatego problem refaktoringu opdada. Problemem jest tylko to, że testuję swojego mocka (choć w testach to jawnie widzę, że wstrzykuję sobie DataProvider), dlatego nie wykluczam potrzeby pisania testów integracyjnych.

7

Takie rzeczy testuje się tak:

  • startujemy aplikacje (@SpringBootTest) z bazą in memory
  • wykonujemy test
  • robimy check, czy zmiany w bazie się pojawiły (czyli stukamy do tej bazy in memory i patrzymy co w niej siedzi)

Pomysły z mockowaniem repozytoriów czy inne cuda to serio :D

0
Shalom napisał(a):

Takie rzeczy testuje się tak:

  • startujemy aplikacje (@SpringBootTest) z bazą in memory
  • wykonujemy test
  • robimy check, czy zmiany w bazie się pojawiły (czyli stukamy do tej bazy in memory i patrzymy co w niej siedzi)

Pomysły z mockowaniem repozytoriów czy inne cuda to serio :D

Ostatnio miałem przypadek, że jak chciałem użyć providera do ORM in-memory, to zostałem skrytykowany, że to nie zadziała, bo nigdy nie działa - okazało się, że używali niestandardowych typów kolumn w jakiejś egzotycznej bazie danych, który akurat ta implementacja in-memory nie wspierała - dlatego zrobili swoją nakładkę na ORM, udawała bazę in-memory - a wystarczyo troszkę konfiguracji :|

0
Shalom napisał(a):

Takie rzeczy testuje się tak:

  • startujemy aplikacje (@SpringBootTest) z bazą in memory
  • wykonujemy test
  • robimy check, czy zmiany w bazie się pojawiły (czyli stukamy do tej bazy in memory i patrzymy co w niej siedzi)

Pomysły z mockowaniem repozytoriów czy inne cuda to serio :D

I czekamy minutę na odpalanie takiego testu. Przy pisaniu logiki biznesowej, odpaladnie co chwilę takiego testu to istna strata czasu. Wolę sobie zmockować repozytorium (przecież to jedna klasa tylko) i odpalać testu na poziomie milisekund, a kiedy wszystko bangla dodać test endpointa tak jak to opisałeś.

1
kkojot napisał(a):

I czekamy minutę na odpalanie takiego testu. Przy pisaniu logiki biznesowej, odpaladnie co chwilę takiego testu to istna strata czasu. Wolę sobie zmockować repozytorium (przecież to jedna klasa tylko) i odpalać testu na poziomie milisekund, a kiedy wszystko bangla dodać test endpointa tak jak to opisałeś.

Bywa różnie. Jak pisałem testy z Cassandrą in memory to najwięcej czasu zajmowało czyszczenie Cassandry. Więc jej nie czyściłem i zawsze generowałem nowe uuidy. W rezultacie miałem zadawalającą szybkość testów bo Cassandrę uruchamiałem tylko raz przed wszystkimi testami.

0

Za mało kodu w tej metodzie żeby ją testować.
Zresztą zwykle nie testuje się ORMa.
Co najwyżej mapowanie do niego.
Napisz test integracyjny do tego, a jednostkowe olej.
https://www.baeldung.com/spring-boot-testing

0

I czekamy minutę na odpalanie takiego testu

@kkojot tak, powtarzamy dalej takie mity xD Widac, że chyba nigdy w życiu takiego testu nie robiłeś, albo klepiesz jakiś turbo monolit. Mam całą masę takich testów i nigdy nie zauważyłem żeby mnie jakoś spowalniały albo wykonywały się minutę :)

Wolę sobie zmockować repozytorium (przecież to jedna klasa tylko) i odpalać testu na poziomie milisekund

Świetny plan, tylko równie dobrze możesz nie odpalać tych testów wcale, bo przeciez taki zmockowany test niczego nie sprawdza... No ale jeśli dzięki temu masz spokojne sumienie...

0
Shalom napisał(a):

Świetny plan, tylko równie dobrze możesz nie odpalać tych testów wcale, bo przeciez taki zmockowany test niczego nie sprawdza... No ale jeśli dzięki temu masz spokojne sumienie...

Nie zgadzam się. Taki test wywali się, jeżeli np. usunę linijkę repository.save(). Przejdzie błędnie tylko w wyjątkowych sytuacjach, np. podczas problemu z serializacją przy zapisie do bazy danych.
Podczas refaktoringu kodu, jak sobie wydzielam klasy, przenoszę metody itp. to CTRL + F5 na testach idzie praktycznie co kilkanaście sekund. P%^%&^^* czekać wtedy, aż mi cały kontekst springa wystartuje ;)

0
Shalom napisał(a):

Świetny plan, tylko równie dobrze możesz nie odpalać tych testów wcale, bo przeciez taki zmockowany test niczego nie sprawdza... No ale jeśli dzięki temu masz spokojne sumienie...

Sprawdza samą logikę, to nie jest nic ;)

1

Ja tam wolę poczekać nawet kilkanaście sekund i mieć normalne testy. Wtedy:

  • testy zielone <=> kod działa, testy czerwone <=> kod nie działa (no chyba że test jest skopany :P)
  • żeby napisać test dla serwisu nie trzeba się zagłębiać w to, jak gdzieś pod spodem są używane repozytoria
  • refactor bebechów nie powoduje konieczności naprawiania testów
  • można wykryć, gdy w adnotacjach JPA jest coś namieszane i aplikacja nie działa jak trzeba lub się wywala

Miałem okazję być w projekcie, w którym repozytoria JPA były zamockowane. I nawet jak testy przechodziły, to dla pewności ręcznie przeklikiwałem aplikację. Jeżeli dostajemy sytuację typu: {testy zielone, aplikacja nie działa} lub {testy czerwone, aplikacja działa}, to komu to potrzebne?

0

@Shalom: Jakiej magii używasz, że z testcontainers masz czasy wykonania poniżej minuty?
Też używam tych kontenetów, i w moich testach leci po prostu request http pod dany endpoint, a dane są faktycznie zapisywane/wyciągane z bazy w kontenerze (w moim przypadku cassandra). Wtedy mam pewność, że dany endpoint działa tak jak zamierzam. W przypadku mockowanych repozytoriów czy in memory, nadal muszę zapewniać takie rzeczy jak to, że nie można querować po polach niebędących kluczami głównymi - w cassandrze tak nie można.
I w sumie wszystko mi się sprawdza, błędy wykrywam wcześnie, nie mam false positive'ów, tylko czas wykonania mógłby być trochę szybszy...

1

Teraz uwaga, można testować jednostkowo bez mockow! Zamiast mockowac DAO z Mockito możesz zrobić implementację np z użyciem HashMapy. Uwaga: działa szybko, I można rzeczywiście łatwo cos zweryfikować, np czy po metodzie save hashmapa zawiera dany obiekt. A zamiast mockowac inne serwisy biznesowe tworzysz cały graf obiektów czyli jednostkę która testujesz. Oczywiście testy integracyjne dalej warto mieć, nie neguje :)

2

Najlepsze, że jak to klasyczne jpa, to często wywalenie linijki z save nic nie zmieni - nadal zmiani będą zapisywane w bazie danych.
testy na mockach czerwone, a aplikacja działa :-)

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