Jak powinien wyglądać test w przypadku CRUD'a

0

Regularnie pojawiają się dyskusje, czy testować "integracyjnie", czy "jednostkowo" i jedyne co wiem, to to, że chyba nikt na świecie nie wie, czym się różnią oba rodzaje testów. Spróbujmy na przykładzie. Mamy sobie CRUD'a do zapisywania jakiegoś pojedynczego typu (w przykładzie UserDTO). Czy powinienem przetestować ten serwis tak jak w przykładzie, czy rozbić test na kawałki? Nie pytam o to, czy nie pominąłem jakiegoś przypadku, czy gdzieś powinienem dodać kolejną asercję, tylko czy powinienem mieć ileś tam testów na poszczególne metody, czy raczej taki pełniejszy scenariusz biznesowy, który testuje system jako ~black box'a.

internal class UsersModuleKtTest {

    @Test
    fun `post put get delete test`() = testApplication {
        val client = createClient {
            install(ContentNegotiation){
                json()
            }
        }
        application {
            install(Koin){
                modules(dbModule)
            }
            usersModule()
        }

        val emptyListResponse = client.get("/users"){
            accept(ContentType.Application.Json)
            contentType(ContentType.Application.Json)
        }

        assertEquals(HttpStatusCode.OK, emptyListResponse.status)
        assertTrue(emptyListResponse.body<List<UserDto>>().isEmpty())

        val postUserResponse = client.post("/users") {
            contentType(ContentType.Application.Json)
            accept(ContentType.Application.Json)
            setBody(UserDto(null, "John", "Smith"))
        }

        val savedUser = postUserResponse.body<UserDto>()

        assertEquals(HttpStatusCode.Created, postUserResponse.status)
        assertNotNull(savedUser.userId)

        val putResponse = client.put("/users/{userId}") {
            url.parameters.set("userId", savedUser.userId.toString())
            contentType(ContentType.Application.Json)
            accept(ContentType.Application.Json)

            setBody(UserDto(savedUser.userId, "Alice", "Doe"))
        }

        assertEquals(HttpStatusCode.OK, putResponse.status)

        val getUserResponse = client.get("/users/{userId}"){
            url.parameters.set("userId", savedUser.userId.toString())
            contentType(ContentType.Application.Json)
            accept(ContentType.Application.Json)
        }

        assertEquals(HttpStatusCode.OK, getUserResponse.status)
        assertEquals(savedUser.userId, getUserResponse.body<UserDto>().userId)
        assertEquals(savedUser.firstName, getUserResponse.body<UserDto>().firstName)
        assertEquals(savedUser.lastname, getUserResponse.body<UserDto>().lastname)

        val singleUserResponse = client.get("/users"){
            accept(ContentType.Application.Json)
            contentType(ContentType.Application.Json)
        }

        assertEquals(HttpStatusCode.OK, singleUserResponse.status)
        assertEquals(1, singleUserResponse.body<List<UserDto>>().size)
    }
}
1

tl;dr; wiele małych testów jest lepsze niż mało dużych


No, na pewno nie powinien wyglądać tak jak ten.

Zanim odpowiem na pytanie, to muszę ogarnąć temat testy integracyjne vs jednostkowe. Nie wiem czy istnieje inny termin w programowaniu tak niezrozumiały jak te dwa słowa - bo one przez znakomitą większość programistów są rozumiane na opak. Idea testów jednostkowych miała zapewnić testowanie programu jako całość; a używa się nazwy "jednostkowy" do testów malutkich kawałków; i dochodzi do sytuacji w której "zamiast wymyślać konkretną potrzebe -> i potem dobrać do niej nazwę", dochodzi do odwrotnego zjawiska gdzie ktoś najpierw wybiera nazwę (np jednostkowy, albo integracyjny) i dopiero potem próbuje napisać test pod tą nazwę. To oczywiście nie ma żadnego sensu.

Myślę że możemy ostrożnie założyć, że te słowa są pomiędzy sobą tak wymieszane, i istniej tak ogromna ilość opinii i artykułów na ten temat, że używanie tych określeń na testy nie ma żadnego sensu, bo nawet jeśli powiesz komuś "napisałem test jednostkowy", to ten ktoś i tak nie zrozumie co masz na myśli. Jeśli się zagłębisz w historyczne początki pierwszych użyć tych nazw, to znajdziesz racjonalne ich rozróżnienie; ale ono dawno zostało zapomniane, i teraz te słowa są używane bardzo naprzemiennie. "Z grubsza" amatorzy rozumieją określenia "integracyjny"/"jednostkowy" jako testy o małym i dużym zakresie; ale niekoniecznie. Inni rozumieją te określenia jako "wolne i szybkie testy"; albo "testy stawiające apke i testy niestawiające apki"; jeszcze inni rozumieją że testy z mockami to jednostkowe, a testy bez mocków to integracyjne - także w moim rozumieniu, te określenia teraz już nie mają sensu.

Nie próbuj się wpasować w kategorię "test integracyjny" albo "test jednostkowy"; staraj się pisać dobre testy, i nie staraj się wpisać w żadną etykietkę, nic dobrego z tego nie wyjdzie.

Pytanie kolejne, to jak dobrze chciałbyś napisać te testy - czyt. jak bardzo ze swojej strefy komfortu chciałbyś wyjść, żeby napisać je odpowiednio. Jeśli mało, to pierwsze co powinieneś zrobić to podzielić ten test na mniejsze kawałki (no i drugie pytanie, czy ten UserDto to jest encja która siedzi w bazie danych? Bo jeśli tak, to należałoby się jej pozbyć z tych testów).

Poza tym powinieneś też się zdecydować czy testujesz cruda, czy interfejs http tego cruda - bo to są różne rzeczy i powinny być przetestowane osobno.

Pamiętaj też że dookoła testów panuje masa nieporozumień i błędnych przekonań, i czasem ludzie bardzo lubią dodawać do nich rzeczy, które tak na prawdę przeszkadzają.

0

Pisz tak, aby testy i sposób ich pisania Cię nie oślepiły.

Koncentrując się na obróbce jesteś w przegranej pozycji, bo możesz przez to przegapić to co najważniejsze. Co z tego, że masz testy skoro potencjalnie mogą rozwiązywać niewłaściwy problem? Choćbyś miał i 1000 testów, one i tak nie ułatwią Ci obrotu o 180'. Dlatego nie raz warto zrobić krok wstecz, ograniczyć liczbę testów, a niekiedy podważyć ich sens.

Jak wiadomo testy to żaden dowód, one nie służą po to by dać gwarancję na brak błędów. Testy są tańsze niż dowód, bo w sumie tylko wyliczasz przypadki, ale też są pewne granice:

  1. pisząc testy średnio opłaca się testować zachowań, których nie powinno być, wystarczy zmiana w apce, a test nie wykryje czego ma nie być (mimo że jest z drobną zmianą).

  2. też testować ciężko jest wyszukane rzeczy. Co z tego, że możesz tak zniwelować szanse na błąd skoro wyprodukowanie testu kosztuje 10 razy więcej niż sam kod.

Te i inne rzeczy w mojej ocenie sprawiają, że więcej błędów unikniesz jeśli zaczniesz częściej i głębiej myśleć o problemie jaki rozwiązujesz niż o samym teście.

Jak już piszę testy to rozgraniczam dwa przypadki:

  1. funkcjonalność wtedy piszę je z samej góry, jeśli mogę z selenium to właśnie to jest dla mnie góra.

  2. złożoną logikę biznesową (możliwie oddolnie, trzymając się jak najbliżej implementacji)

0

OK, to, żeby chwilowo kod nie zaciemniał obrazu, widzę to tak:
Wiem, że mam do napisania funkcjonalność, która będzie pozwalała tworzyć, odczytywać, zmieniać i usuwać konta użytkowników. Ta funkcjonalność jest wymaganiem biznesowym, czyli jest to jakiś kawałek większej definicji "działania aplikacji".

Czy waszym zdaniem test:

sprawdź, czy baza jest pusta()
utwórz konto użytkownika()
sprawdź czy konto użytkownika istnieje()
dokonaj zmian na tym koncie()
sprawdź czy zmiany się zapisały()
usuń konto()
sprawdź, czy się usunęło()

Jest właściwym podejściem? Bo plusy tego są takie, że:

  • Wiemy, że aplikacja jest w stanie zrobić to, czego się od niej oczekuje, przynajmniej raz
  • Możemy wymienić całą aplikację i tak długo jak api się nie zmienia, to testy działają i nie wymagają zmian
  • Da się taki test zgeneralizować do każdej domeny obiektowej (faktury, zamówienia, whatever), która pojawi się w projekcie.

Są też minusy

  • test nie jest czytelny
  • test jest złożony, czyli jest większe prawdopodobieństwo, że pojawią się w nim błędy
2
piotrpo napisał(a):
sprawdź, czy baza jest pusta()
utwórz konto użytkownika()
sprawdź czy konto użytkownika istnieje()
dokonaj zmian na tym koncie()
sprawdź czy zmiany się zapisały()
usuń konto()
sprawdź, czy się usunęło()

moim zdaniem można by to było rozbić na 3 testy łatwiej się połapać jeżeli cos się wykrzaczy, wydaje mi się tez, że byłoby to wygodniejsze bo dla każdego z nich znalazłoby się tez parę przypadków /TestCase/, i nie koniecznie skończy się na takiej malej ilości testów.

0

W teorii małe testy są lepsze od dużych bo dają większą granulację - tj. jeśli zepsujemy coś w MyAwesomeService to wywalić nam się powinien MyAwesomeServiceTest. W praktyce czasem łatwiej jest określić, jak powinien zachowywać się serwis na dużych kawałkach, niż tworzyć duże przypadki na małych kawałkach.

W przypadku CRUDów takie sytuacje jednak rzadko się zdarzają bo warstwy są mocno izolowane od siebie, a ich odpowiedzialność jest też dobrze zdefiniowana. Dodatkowym plusem jest to, że pisanie małych testów po prostu ułatwia pisanie całości - więc ogólnie w przypadku tradycyjnych CRUDów trzymałbym się małych testów.

Jeśli chodzi o test to jest on po prostu zbyt rozwlekły. Możesz spokojnie go rozdzielić na:

  • tworzenie konta w przypadku, gdy konto o takiej nazwie istnieje
  • tworzenie konta w przypadku, gdy konto o takiej nazwie nie istnieje
  • zapisywanie zmian na koncie
  • usuwanie nieistniejącego konta
  • usuwanie istniejącego konta
0
piotrpo napisał(a):

OK, to, żeby chwilowo kod nie zaciemniał obrazu, widzę to tak:
Wiem, że mam do napisania funkcjonalność, która będzie pozwalała tworzyć, odczytywać, zmieniać i usuwać konta użytkowników. Ta funkcjonalność jest wymaganiem biznesowym, czyli jest to jakiś kawałek większej definicji "działania aplikacji".

Czy waszym zdaniem test:

sprawdź, czy baza jest pusta()
utwórz konto użytkownika()
sprawdź czy konto użytkownika istnieje()
dokonaj zmian na tym koncie()
sprawdź czy zmiany się zapisały()
usuń konto()
sprawdź, czy się usunęło()

No tak, w skrócie tak. Pod warunkiem że "sprawdź czy baza jest pusta" oraz "sprawdź czy się usunęło" nie wołają bezpośrednio do bazy, tylko sprawdzają to pozostałymi metodami CRUD'a.

  • Wiemy, że aplikacja jest w stanie zrobić to, czego się od niej oczekuje, przynajmniej raz
  • Możemy wymienić całą aplikację i tak długo jak api się nie zmienia, to testy działają i nie wymagają zmian
  • Da się taki test zgeneralizować do każdej domeny obiektowej (faktury, zamówienia, whatever), która pojawi się w projekcie.

Są też minusy

  • test nie jest czytelny
  • test jest złożony, czyli jest większe prawdopodobieństwo, że pojawią się w nim błędy

Czemu test nie jest czytelny? Moim zdaniem byłby bardzo czytelny. test jest złożony Czemu miałby być bardziej złożony? Moim zdaniem nie jest.

0

@Riddle: Spadek czytelności wynika z rozmiaru - jest dłuższy, więc siłą rzeczy będzie mniej czytelny od serii małych scenariuszy typu:

upewnij się, ze Janka nie ma w serwisie(GET)
wstaw Janka(POST)
sprawdź, czy obiekt został stworzony (GET)

I oczywiście mam na myśli testowanie systemu na poziomie zewnętrznego API, to gdzie te dane są fizycznie przechowywane, to już detal implementacyjny. No i dodatkowa zaleta granularnych testów, jak coś walnie, to obszar poszukiwań jest mniejszy.
Do tego rozwlekłość testu, powoduje, że chcemy go podzielić na kawałki i pojawia się kolejne pytanie, co testuje te kawałki.

Oczywiście tak sobie gdybam i szukam dziury w całym.

2
piotrpo napisał(a):

I oczywiście mam na myśli testowanie systemu na poziomie zewnętrznego API, to gdzie te dane są fizycznie przechowywane, to już detal implementacyjny. No i dodatkowa zaleta granularnych testów, jak coś walnie, to obszar poszukiwań jest mniejszy.
Do tego rozwlekłość testu, powoduje, że chcemy go podzielić na kawałki i pojawia się kolejne pytanie, co testuje te kawałki.

Oczywiście tak sobie gdybam i szukam dziury w całym.

Nie no, tutaj masz rację.

piotrpo napisał(a):

@Riddle: Spadek czytelności wynika z rozmiaru - jest dłuższy, więc siłą rzeczy będzie mniej czytelny od serii małych scenariuszy typu:

upewnij się, ze Janka nie ma w serwisie(GET)
wstaw Janka(POST)
sprawdź, czy obiekt został stworzony (GET)

Aha, nie no to oczywiście że musisz go podzielić na jeszcze mniejsze kawałki. Myślałem że jak wypisałeś te elementy: sprawdź, czy baza jest pusta(), utwórz konto użytkownika(), sprawdź czy konto użytkownika istnieje(), dokonaj zmian na tym koncie(), sprawdź czy zmiany się zapisały(), usuń konto(), sprawdź, czy się usunęło() to mówisz o sześciu osobnych testach :D

Testy powinny być malutkie, najmniejsze jak się da. Jeśli jesteś w stanie rozbić test na dwa mniejsze - zrób to. Testy powinny wyglądać jakoś tak (kod poglądowy):

# given
app = application_with_no_users
# when
response = app.get_user() # to zawoła GET /users albo GET /user/:id pod spodem
# then
assert response. jakoś tutaj sprawdź czy zwróciło Ci jakieś info o braku usera
# given
app = application_with_one_user # tutaj pod spodem zawołaj POST /user żeby stworzyć jakiegoś usera
# when
response = app.get_user()
# then
assert response.user # tutaj zrób asercje która sprawdzi czy jest dodany tutaj user
# when
app = application_with_no_users
# when
app.add_user("Marek") # tutaj strzel pod POST /user pod spodem
# then
user = app.get_user # tutaj strzel pod GET /user/marek jakoś
assert user.name == "Marek"
# given
app = application_with_one_user
# when
app.delete_user("marek")
# then
user = app.get_user  # tutaj strzel pod GET /user/marek jakoś
assert response # zrób jakąś asercje żeby sprawdzić czy ten user co go właśnie wczytałeś nie istnieje

No i tak dalej, dla każdej metody i funkcjonalności, np:

# given
app = application_with_one_user(name="Marek")
# when
app.add_user("Marek")  # duplikacja imienia
# then
users = app.get_users_by_name("Marek")
assert len(users) == 1 # upewnij się że nowy user się nie dodał
0

Tak, ale z drugiej strony (wiem, że kłócę się sam z sobą i w sumie tak jest...), pełen scenariusz testowy (taki długi...), obejmuje pełen cykl życia obiektu, co też (chyba) wnosi jakąś wartość, bo to, że jesteśmy w stanie wykonać pojedyncze zmiany na różnych obiektach, nie oznacza jeszcze, że będzie możliwe wykonanie sekwencji zmiany stanów na pojedynczym obiekcie.

2
piotrpo napisał(a):

Tak, ale z drugiej strony (wiem, że kłócę się sam z sobą i w sumie tak jest...), pełen scenariusz testowy (taki długi...), obejmuje pełen cykl życia obiektu, co też (chyba) wnosi jakąś wartość,

To jest Twoja intuicja i próba zrozumienia tego procesu jako człowiek, a nie faktyczna wiedza wynikająca z praktyk programistycznych.

piotrpo napisał(a):

bo to, że jesteśmy w stanie wykonać pojedyncze zmiany na różnych obiektach, nie oznacza jeszcze, że będzie możliwe wykonanie sekwencji zmiany stanów na pojedynczym obiekcie.

Oznacza. Jeśli napisałeś 10 malutkich testów na kawałek "scenariusza", to one znajdą wszystkie błędy które byłby w stanie znaleźć jeden duży test. Tzn. jeśli dodatkowo napiszesz ten "długi test" to on nie będzie w stanie znaleźć niczego, czego te 10 małych testów by nie znalazły. W nomenklaturze TDD, nie będziesz w stanie spowodować że ten długi test sfailuje, w momencie w którym te małe testy nie failują - Oczywiście zakładając że w tych małych testach nie ma błędów.

Pamiętaj że odpowiednie # given to gwarantuje, tzn. przed wykonaniem testu powinieneś doprowadzić aplikację do pewnego znanego stanu; i tym znanym stanem może być dokładnie ten stan w jakim apka została zostawiona przez poprzedni test; ale już bez zależności na ten poprzedni test. Jak napiszesz "duży długi test" to dodajesz tą zależność, tylko że nie jawnie.


Jeśli masz trzy pliki w katalogu, to żeby je usunąć wystarczy zrobić

rm one.txt
rm two.txt
rm three.txt

Możesz dodatkowo uruchomić jeszcze

rm *

ale on już jest niepotrzebny (chyba że tak na prawdę w katalogu było więcej plików, ale wtedy te pierwsze rm powinny być zaktualizowane).

1

a scenariusz biznesowy to nie będzie już e2e :p ?

Ja podchodzę do tego od strony praktycznej tj szybki i dłuższe testy. W klasach które stawiają context apki czy uruchamiają kontenery do testów daje na koncu suffix IT ale ogólnie potrzebne mi to jest tylko do selektorow aby łatwo uruchamiać „szybkie” testy które uruchamiam lokalnie podczas pracy i długie które zazwyczaj uruchamiam po dłuższym dewelopowaniu lub wgl zostawiam to do uruchomienia na gitlabie.

Btw mam aplikacje w której testy e2e chodzą ok 3h. Chyba by mnie powaliło jakbym miał to uruchamiać lokalnie xD

0
Schadoow napisał(a):

a scenariusz biznesowy to nie będzie już e2e :p ?

Ja podchodzę do tego od strony praktycznej tj szybki i dłuższe testy. W klasach które stawiają context apki czy uruchamiają kontenery do testów daje na koncu suffix IT ale ogólnie potrzebne mi to jest tylko do selektorow aby łatwo uruchamiać „szybkie” testy które uruchamiam lokalnie podczas pracy i długie które zazwyczaj uruchamiam po dłuższym dewelopowaniu lub wgl zostawiam to do uruchomienia na gitlabie.

Moim zdaniem nie powinieneś używać do tego suffixów, tylko elementów runnera testów, np w pytest jest @mark, w jUnit jest @group, możesz pooznaczać testy tagami "slow" i "fast", i potem uruchamiać albo szybkie, albo wolne testy według konieczności. Możesz też napisać fixture który najpierw odpala wszystkie szybkie, a na samym końcu wszystkie wolne - krótszy feedback loop. Możesz też zrobić, że test sam z siebie dodaje tag "slow", jeśli wykonuje operacje które są ciężkie (typu, jeśli wywołasz metodę SetupContextApp(), to "dodaj tag slow z automatu).

Schadoow napisał(a):

Btw mam aplikacje w której testy e2e chodzą ok 3h. Chyba by mnie powaliło jakbym miał to uruchamiać lokalnie xD

Kogoś powaliło że napisał testy które trwają 3h ;| To jest bardzo słabe że jest na to zgoda w zespole, i faktycznie przed deployem trzeba czekać 3h na pełen suite. Przecież tak się nie da pracować. I bardzo fajna konsekwencja - nie odpalasz testów lokalnie, tylko się odpalają na gitlabie - czyli nie widzisz rezultatu z testów bezpośrednio, tylko dopiero jak zostało to odroczone.

Mam nadzieję że się nie obrazisz, jeśli powiem że rady od kogoś kto ma w projekcie testy które się wykonują 3h i który nie widzi w tym nic dziwnego; jako mniej godne zaufania? :>

1

@Riddle: nie obrazę się. Aczkolwiek jestem z nich zadowolony bo testują mi cała logikę biznesowa i od 2016 nie mieliśmy zgłoszeń produkcyjnych. Co prawda nie jest to apka której uzywa dużo osób ok 500 osób dziennie.

Przy czym średnio widze szanse aby to przyspieszyć xD. Bo to nie chodzi o wielkość testów tylko liczbę use casow i ścieżek użytkowników. Dla przykładu dla bazy oracla kiedyś czytałem ze testy chodzą dwa dni xD

0
Schadoow napisał(a):

@Riddle: nie obrazę się. Aczkolwiek jestem z nich zadowolony bo testują mi cała logikę biznesowa i od 2016 nie mieliśmy zgłoszeń produkcyjnych. Co prawda nie jest to apka której uzywa dużo osób ok 500 osób dziennie.

No, to bardzo fajnie. Ale na prawdę dobre testy to takie które również testują całą logikę biznesową, i takie dzięki którym nie ma wyjątków produkcyjnych oraz wykonują się bardzo szybko, powiedzmy w ciągu max. 5-10 minut.

Robimy offtop, @piotrpo pytał o coś innego.

0

Z tym czasem trwania testów, to trochę zależy od tego ile razy kontekst aplikacji jest stawiany, jak ciężki jest ten kontekst. To wyżej, to jakieś tam moje wprawki z pet project, gdzie przy okazji rozpoznaję działanie ktor i jego narzędzi wspomagających pisanie testów. Tutaj jest to "niby-klient" http. Ten test wyżej, przy aktualnie zamokowanej w pamięci bazie wykonuje się w kilkadziesiąt ms, więc teoretycznie można mieć tych scenariuszy dużo.

0
piotrpo napisał(a):

Z tym czasem trwania testów, to trochę zależy od tego ile razy kontekst aplikacji jest stawiany, jak ciężki jest ten kontekst. To wyżej, to jakieś tam moje wprawki z pet project, gdzie przy okazji rozpoznaję działanie ktor i jego narzędzi wspomagających pisanie testów. Tutaj jest to "niby-klient" http. Ten test wyżej, przy aktualnie zamokowanej w pamięci bazie wykonuje się w kilkadziesiąt ms, więc teoretycznie można mieć tych scenariuszy dużo.

Testy które się wykonują długo (powiedzmy dłużej niż 10 minut) są całkowicie nieakceptowalne. Każdy kto mówi że takie testy są spoko

No bo patrz, jeśli testy trwają długo to po prostu ludzie ich nie będą odpalać. Zostawią to na koniec dnia, albo odroczą do builda w gitlabie (tak jak @Schadoow). Ale żeby dobrze developować software, to musisz odpalać testy często, najczęściej jak się da, żeby pomogły w developmencie. Niestety test których odpalenie zostawia się na koniec dnia albo odpala w gitlabie nie pomagają w tym; więc stają się dużo gorzej i przynoszą dużo mniejszą wartość.

Z resztą, jakby to miało wyglądać. Odpalasz testy, czekasz 3h, jeden z ostatnich testów failuje; dodajesz poprawkę, znowu odpalasz testy; i za 3h dowiadujesz sie że gdzieś brakuje nawiasu albo spacji. Tak się nie da pracować. Równie dobrze mógłbyś w ogóle nie mieć testów.

Dla mnie, jedyny przypadek gdzie testy miałyby trwać 3h, to jest wtedy gdyby było około 300tys. przypadków testowych - ale to jest bardzo dużo, i nie sądzę że jakieś aplikacje mają coś takiego. Jeśli jest ich np 10tys. czyli taki standard mniej więcej, to 10 minut to jest max.

piotrpo napisał(a):

Z tym czasem trwania testów, to trochę zależy od tego ile razy kontekst aplikacji jest stawiany, jak ciężki jest ten kontekst.

Ten "kontekst aplikacji" to jest szczegół implementacyjny, należałoby go podmienić jeśli sprawia takie problemy.

0

To może temat na inny wątek ale osobiście w testach testujących „cała aplikacje”. Nie znalazłem rozwiązania aby osiągnąć separacje testów i jednocześnie benefit z tego ze stawiam cała infre. Przez to mogę testować zachowanie aplikacji i łatwo podbijać zależności np migracja wersji bazy danych czy update innych 3rd-part serwisów.

Bo jasne mogę testować in memory ale wtedy musiałbym do każdego serwisu zewnetrznego którego używam dopisać testy kontraktu co np w przypadku bazy danych może być trudne :)

0

A to nie zależy trochę od rozmiaru testowanej aplikacji? Bo taki crud, to pójdzie szybko, ale texty black box systemu, który składa się z iluśtam dziesięciu mikrousług komunikujących się przez inne usługi (np. chmurowe), już lekki i szybki nie będzie.

1
Schadoow napisał(a):

Ale na produkcji aplikacja komunikuje się przez połączenie sieciowe a nie między-procesowo. Podam przykład z życia, złe wybrałem implementacje z biblioteki javy do czytania plików przez co skipy zamiast natywnych wywołań robiły read i pomijały wynik czego pierwszym objawem było zapchanie się buforów na karcie sieciowej i wywalenie całego networku na clustrze. Czy twoje testy „między-procesowe” wylapaly by coś takiego ?

Mylisz dwie rzeczy jednocześnie, czyli testy zachowań biznesowych oraz testy niskopoziomowych mechanizmów - to są dwie różne rzeczy i powinny być przetestowane osobno. To że próbujesz je wrzucić do jednego wora, to najpewniej jeden z powodów czemu testy trwają 3h.

Zróbmy krok wstecz i obadajmy temat na spokojnie.

  • Masz powiedzmy 1000 przypadków testowych. Jeśli uruchomisz je tak, że każdy z nich wykona request przez kartę sieciową, to każdy z nich potrwa między 100ms-1000s; około można liczyć że potrwa to od 100-1000s, czyli około półtorej minuty do 16 minut. To już jest sporo za dużo. Można by ten czas skrócić, jeśli wyeliminować by z tych requestów overhead, czyli właśnie ruch między sieciowy.
  • Ale, jest pewien aspekt sieciowy który musisz przetestować, mianowicie różne niskopoziomowe elementy które pojawiają się tylko przy faktycznych requestach sieciowych; więc również musisz je przetestować;

No i jak tu pogodzić te dwie rzeczy? Wbrew pozorom bardzo prosto, tylko trzeba się oderwać od miskoncepcji i błędnych przekonań które nosimy w głowie. Napisz 1000 testów Twoich przypadków biznesowych na "lekkiej" podstawie, czyli takiej gdzie komunikacja jest najszybsza jak się da, np komunikatami w systemie operacyjnym albo inny sposób komunikacji między procesami. A potem, dodatkowo, napisz testy które nie sprawdzają przypadków testowych (więc nie musi być ich 1000), ale sprawdzają tylko czy ruch po siedzi działa tak jak powinien.

Wynikiem będzie 1000 testów które sprawdzają zachowania biznesoweg, oraz 10-20 testów które owszem są wolne, ale za to sprawdzają niskopoziomowe mechanizmy. Tych drugich testów nie musi być 1000, musi ich być tyle żeby wykazać że ruch po siedzi idzie sprawnie; będzie ich mało więc wykonają się szybko. Wynikiem będzie bardzo dobry test suite który wykonuje się szybko, a dodatkowo testuje niskopoziomowe mechanizmy.

Twoim błędem jest przekonanie, że jeśli mają być jakieś testy niskopoziomowe (a mają, bo chcesz to przetestować), to to oznacza że wszystkie testy muszą takie być - a nie muszą.

1
piotrpo napisał(a):

Czy powinienem przetestować ten serwis tak jak w przykładzie, czy rozbić test na kawałki? Nie pytam o to, czy nie pominąłem jakiegoś przypadku, czy gdzieś powinienem dodać kolejną asercję, tylko czy powinienem mieć ileś tam testów na poszczególne metody, czy raczej taki pełniejszy scenariusz biznesowy, który testuje system jako ~black box'a.

Możesz zastosować oba podejścia. Taki test akceptacyjny jest spoko, bo mówi nam czy nasza aplikacja robi, to co miała robić. Ale oprócz niego potrzebujemy dodatkowych, mniejszych testów, które będą też pokrywać inne przypadki.

0

Czy to jest unit test? Nie, bo testujesz wiele unitów jeden po drugim. To czy robisz to przez warstwę http według mnie nie ma żadnego znaczenia. Gdybyś przetestował tylko i wyłącznie endpoint z getem to byłby to unit test.

Czy to jest integration test? https://martinfowler.com/articles/practical-test-pyramid.html#IntegrationTests - ta definicja nie zabrania wielu asercji w jednym integration teście.

Czy to jest acceptance test? Ktoś mógłby powiedzieć, że brakuje tutaj podpiętego scenariusza bdd.

Czy lepsze jest dużo unit testów czy jeden acceptance test? Według mnie to zależy od jakości kodu i wydatków na testy. Jeżeli klasy nie mają odpowiednio wydzielonych odpowiedzialności i api to unit testy będą ciągle się zmieniały przez co będziesz wolniej dostarczał nowe funkcjonalności. Jeżeli jesteś w stanie wydzielić niezależne moduły, które nie mają side effektów, które musisz mockować na różne dziwne sposoby to warto inwestować w unit testy. W przeciwnym wypadku według mnie lepiej inwestować w acceptance/bdd testy. Ewentualnie testy integracyjne.

0

Kolejną miskoncepcją z którą warto się zmierzyć jest "Proste rzeczy łatwo się testuje". Tą miskoncepcję (mit) bardzo trudno wykorzenić, bo większość programistów uczy się pisać aplikacje od aplikacji konsolowych - hello worldy; i w danym języku własnie od tego z reguły zaczynami: print()/input() w pythonie, czy System.out.println()/Scanner(System.in) w Javie, etc. Czyli już na starcie piszemy aplikacje które mają zależności - na wyjście i wejście standardowe procesu w którym uruchamiamy naszą aplikację.

Pisząc dobre testy, chcemy przetestować takie rzeczy poprawnie; ale bardzo często język ani biblioteka standardowa nie udostępnia nam łatwych narzędzi żeby to przetestować - musimy to więc jakoś ogarnąć sami; i niestety napisanie testu który poprawnie sprawdza stdin/stdout - czyli najbardziej podstawowe rzeczy od których się zaczyna jest względnie trudne. Osoba która zaczyna programować nie będzie umiała tego zrobić; a osoby które już programują długo często nie decydują się tego robić; mimo że powinny.

Ale pamiętajmy - to żę w jakimś języku lub frameworku nie ma łatwych narzędzi do przetestowania tego; nie znaczy że my nie powinniśmy sami tego przetestować - jeśli zajdzie potrzeba, trzeba opisać takie testy samemu, niestety.

twoj_stary_pijany napisał(a):

Czy to jest unit test? Nie, bo testujesz wiele unitów jeden po drugim. To czy robisz to przez warstwę http według mnie nie ma żadnego znaczenia. Gdybyś przetestował tylko i wyłącznie endpoint z getem to byłby to unit test.

Czy to jest integration test? https://martinfowler.com/articles/practical-test-pyramid.html#IntegrationTests - ta definicja nie zabrania wielu asercji w jednym integration teście.

Czy to jest acceptance test? Ktoś mógłby powiedzieć, że brakuje tutaj podpiętego scenariusza bdd.

Czy lepsze jest dużo unit testów czy jeden acceptance test? Według mnie to zależy od jakości kodu i wydatków na testy. Jeżeli klasy nie mają odpowiednio wydzielonych odpowiedzialności i api to unit testy będą ciągle się zmieniały przez co będziesz wolniej dostarczał nowe funkcjonalności. Jeżeli jesteś w stanie wydzielić niezależne moduły, które nie mają side effektów, które musisz mockować na różne dziwne sposoby to warto inwestować w unit testy. W przeciwnym wypadku według mnie lepiej inwestować w acceptance/bdd testy. Ewentualnie testy integracyjne.

Moim zdaniem używanie takich kategorii nie ma sensu; i lepiej mówić o cechach testów: czy jest szybki/wolny, czy ma duży scope czy mały; czy ma dużo zależności czy nie; czy failuje jeśli brakuje mu pewnych zależności; czy zna szczegóły implementacyjne kodu czy nie; czy polega na frameworku czy nie.

Używając takich kategorii jak jednostkowy/integracyjny/akceptacyjny/e2e dojdzie tylko do nieporozumienia bo różne osoby inaczej rozumieją te testy; te określenia polegają na założeniach które właściwie nie istnieją.

0
Riddle napisał(a):

Kolejną miskoncepcją z którą warto się zmierzyć jest "Proste rzeczy łatwo się testuje". Tą miskoncepcję (mit) bardzo trudno wykorzenić, bo większość programistów uczy się pisać aplikacje od aplikacji konsolowych - hello worldy; i w danym języku własnie od tego z reguły zaczynami: print()/input() w pythonie, czy System.out.println()/Scanner(System.in) w Javie, etc. Czyli już na starcie piszemy aplikacje które mają zależności - na wyjście i wejście standardowe procesu w którym uruchamiamy naszą aplikację.

@twoj_stary_pijany: To się nie uda. Te określenia zbyt głęboko weszły w całą naszą branżę, i zakomunikowanie jej teraz "hej, słuchajcie: tutaj jest literatura, teraz tymi definicjami się posługujemy" jest już niemożliwa. Lepiej w ogóle nie posługiwać się tymi terminami, i skupić się na samych kryteriach. — Riddle 2 sekundy temu

Nie rozumiem dlaczego mielibyśmy teraz obniżać standardy do ludzi, którzy piszą kod produkcyjny na zasadzie produkowania side effektów i zrzucania winy na QA, że nie dają rady tego otestować. Jak mam coś otestować i widzę źle zaprojektowany interface to idę do developera i zadaję mu pytanie czy zastanawiał się jakie skutki niesie za sobą projektowanie takiego interface'u. Skutki, które sprawiają, że i on, i inni będziemy zarabiali mniej pieniędzy, a co najmniej odwlecze nam się podwyżka bo zarząd nie będzie widział przyrostu. I zakładam, że jeżeli developer ma dobre intencje w stosunku do projektu to weźmie i przeczyta literaturę i się poprawi. Najlepiej przyjść do projektu, zobaczyć gówniany kod i później płakać na forum, że się zwalniam bo nikt mnie nie rozumie.

I ja się w pełni zgadzam ze stwierdzeniem, że proste rzeczy łatwo się testuje. Z tym, że proste rzeczy niełatwo się pisze, trzeba do tego kompetencji. Napisanie prostego interface'u wymaga lat doświadczeń. Kiedy Messi strzela bramkę to wszystko wydaje się proste, ale nie jest łatwe. Właśnie po to czytamy literaturę, żeby się dokształcać i po to właśnie jest ten temat.

0

@crejk: Jasne, tylko pytanie co to są te inne przypadki. Bo jeżeli aplikacja robi, to co jest opisane w wymaganiach, ten zakres jest pokryty przypadkami testowymi, to powstaje pytanie co robią linijki, które tymi przypadkami pokryte nie są? Chyba, że mówisz o przypadkach negatywnych typu "co się stanie jak poproszę o dane użytkownika, który nie istnieje" itd.

@twoj_stary_pijany: Tu nie chodzi o obniżanie standardów. Obniżenie standardów, to jest korpo nakaz zwiększenia pokrycia testami, które następnie jest realizowane przez gromady przypadków testowych na niskim poziomie (pojedynczej klasy), obudowane mockami.
Podszedłem do tego co napisałem tak, że skoro opisuję testem całe wymagania, dopisuję do tego kod, który te wymagania spełnia, to w wyniku tego działania, przetestowane zostaje zarówno, czy aplikacja robi to co miała robić, jak i to, czy klasy wewnątrz faktycznie robią to, co leży w ich odpowiedzialnościach, oraz czy "dogadują" się ze sobą.

Nie zostaje przetestowana w wyniku tego testu warstwa sieciowa (akurat Ktor pozwala na wpięcie się bezpośrednio do kontrolera, widoczne w teście endpointy można traktować jako "nazwy metod"), oraz połączenie z bazą danych (bo szybkość działania testu i moje lenistwo).

0
piotrpo napisał(a):

@twoj_stary_pijany: Tu nie chodzi o obniżanie standardów. Obniżenie standardów, to jest korpo nakaz zwiększenia pokrycia testami, które następnie jest realizowane przez gromady przypadków testowych na niskim poziomie (pojedynczej klasy), obudowane mockami.
Podszedłem do tego co napisałem tak, że skoro opisuję testem całe wymagania, dopisuję do tego kod, który te wymagania spełnia, to w wyniku tego działania, przetestowane zostaje zarówno, czy aplikacja robi to co miała robić, jak i to, czy klasy wewnątrz faktycznie robią to, co leży w ich odpowiedzialnościach, oraz czy "dogadują" się ze sobą.

Coś co określasz korpo nakazem to są konwencje kultu cargo wymuszone przez konkretnych ludzi, którzy nie czytają literatury. Takich ludzi spotykam u każdego klienta i zwykle, żeby naprawić projekt to trzeba zacząć od zidentyfikowania tej osoby (trudne), a następnie pogadania z nią (rzadko przynosi skutek) lub też jej usunięcia z projektu (nie zawsze możliwe). Ale nie mów mi, że to są korpo nakazy. Bo korpo nie myśli, a ktoś personalnie te nakazy stworzył.

Żeby przetestować unit nie potrzebujesz wszystkiego mockować. Możesz po prostu użyć stubów. Czyli masz moduł, który ma zależność na jakiś obiekt. Tworzysz uproszczony obiekt implementujący interface tych zależności, a następnie wstrzykujesz ten obiekt. Jeżeli dobrze zaprojektujesz warstę domeny biznesowej to możesz w prosty sposób usunąć zależność na bazę danych. To podejście też jest opisane w tym artykule, który podlinkowałem. Oczywiście ten artykuł też nie jest idealny bo wspomina o class testach. Żeby mieć dobry obraz unit testów trzeba przeczytać kapkę więcej.

0

No dobra, przeczytałem ten artykuł, tylko się z nim nie zgadzam.

  1. Trzymanie się uparcie rozumienia Unit, jako "klasy"
  2. Przecenianie znaczenia unit testów

O ile punk 1 to w sumie przedmiot dyskusji w tym wątku i jakaś tam moja opinia, to spróbuję uzasadnić punkt 2 na podstawie tego co w tym artykule widzę:

public class ExampleControllerTest {

    private ExampleController subject;

    @Mock
    private PersonRepository personRepo;

    @Before
    public void setUp() throws Exception {
        initMocks(this);
        subject = new ExampleController(personRepo);
    }

    @Test
    public void shouldReturnFullNameOfAPerson() throws Exception {
        Person peter = new Person("Peter", "Pan");
        given(personRepo.findByLastName("Pan"))
            .willReturn(Optional.of(peter));

        String greeting = subject.hello("Pan");

        assertThat(greeting, is("Hello Peter Pan!"));
    }

    @Test
    public void shouldTellIfPersonIsUnknown() throws Exception {
        given(personRepo.findByLastName(anyString()))
            .willReturn(Optional.empty());

        String greeting = subject.hello("Pan");

        assertThat(greeting, is("Who is this 'Pan' you're talking about?"));
    }
}

Testowana klasa ma odpowiedzialność polegającą na:

  • Odebraniu parametru identyfikującego użytkownika

  • Wyszukaniu konta w repozytorium odpowiadającego temu parametrowi

  • Zwróceniu tekstu powitania na podstawie tych danych.

    Test, który wkleiłem sprawdza wyłącznie ostatnią część tej odpowiedzialności. Ok, możemy mieć jeszcze jakieś testowanie samego repozytorium, ale załóżmy, że ktoś zamiast zwrócić się do repozytorium przez .findByLastName() postanowi pobrać wszystkie konta i przeszukać je lokalnie persons.stream().filter(). Test wysypie się, bo to co sprawdzają pierwsze 2 punkty, to prawidłowa konfiguracja mocka, a nie działanie SuT.

Do napisania tego testu trzeba nie tylko wiedzieć z jakich zależności korzysta klasa, ale też w jaki sposób z nich korzysta i to jest moim zdaniem coś, co mocno obniża wartość tego typu testu.

0
piotrpo napisał(a):

Regularnie pojawiają się dyskusje, czy testować "integracyjnie", czy "jednostkowo" i jedyne co wiem, to to, że chyba nikt na świecie nie wie, czym się różnią oba rodzaje testów.

Ja wiem.

Czy powinienem przetestować ten serwis tak jak w przykładzie, czy rozbić test na kawałki?

Ja u siebie zazwyczaj rozbijam - wtedy od razu widać, który endpoint padł.

0
twoj_stary_pijany napisał(a):

I ja się w pełni zgadzam ze stwierdzeniem, że proste rzeczy łatwo się testuje.

Ale no właśnie często łatwe rzeczy trudno przetestować, niestety.

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.