Jak testować, by nie strzelić sobie w kolano

3

Po części z nadmiaru wolnego czasu, po części przez różne dyskusje techniczne w zespole, a po części pod wpływem tego wątku: Jak w Twojej firmie podchodzicie do tematu testowania? próbuję dojść do jakiegoś (choćby pozornie) jednoznacznego wniosku, jak tak naprawdę należy testować... by nie oberwać potem rykoszetem ;)

Generalnie to taki trochę temat-rzeka, co rusz to inne podejście, każdy robi to trochę inaczej (o ile w ogóle). Choćby z samych rzeczy, które widziałem i/lub popełniłem i/lub o których słyszałem/czytałem, a które mógłbym wymienić jako delikatnie mówiąc niekoniecznie godne pochwały:

  • Brak testów bo po co / bo nie ma czasu / bo lenistwo / bo działa i tyle.
  • Testowanie "tylko tego co jest sens testować" - gdzie okazuje się, że różnorakich potworków też nie było sensu testować.
  • Testowanie, ale poza rzeczami, które zbyt trudno jest przetestować.
  • Testowanie żeby coverage w JaCoCo czy innym toolu się zgadzał.
  • Testy, po których właściwie nie wiadomo, czy system robi wszystko co powinien, no ale niby przechodzą.
  • Pisanie "unit testów" pojedynczych klas z mockami zależności, pisanie "integration testów" bez mocków i testowanie niemalże tych samych przypadków na obie modły.
  • Pisanie "unit testów" do rzeczy takich, jak gettery wygenerowane dla KasztanDTO przez Lomboka.
  • Są sobie testy, są sobie "high level", testują wymagania, a pod spodem jest czarna skrzynka w której zmieścisz słonia razem z sawanną i klasy po 40k SLoC linii.
  • Gruba na metr wylewka z betonu w testach, niczego nie da się ruszyć ani przerobić.
  • Testy przechodzą, więc nie ma co sprawdzać, czy faktycznie wszystko działa.
  • Testy tak odporne na zmiany w kodzie, że właściwie nie wiadomo, jak je zepsuć, więc nie wiadomo, czy coś tak naprawdę testują.
  • Testy nafaszerowane mock .... mock ... spy .... verify .... times .... mock .... verify .... inorder .....
  • Testy które nie dość, że muszą stawiać kontekst aplikacji, to jeszcze odpalają milion upgrade'ów / migracji DB i to przed każdym testem, w efekcie wszystko trwa wieki.
  • Niemal identyczne testy powtarzające się dla wielu różnych komponentów, które re-używają jakiegoś (zwykle springowego) komponentu / walidatora / konfiguracji / aspektu i upewnianie się, że w każdym miejscu to coś zadziałało jak powinno - plus odrębne testy tego komponentu, oczywiście.

Do myślenia dało mi też nagranie z talka Iana Coopera o "upadku" TDD, gdzie mówił o tym, by nie testować szczegółów implementacyjnych, a jeśli już koniecznie chcesz to zrobić, by rozpoznać ogniem jak działa system - to masz je później usunąć. Bo inaczej wpadniesz w masę różnych pułapek testowania, bolączek TDD, będziesz nieefektywny i na domiar złego ogólnie się zniechęcisz.

Ok, spoko. Ma sens.

  • Nie chcemy, by testy zabetonowały kod.
  • Nie chcemy, by byle refaktoring psuł nam testy, bo w końcu wszyscy będą się bać refaktorować.
  • A już tym bardziej nie chcielibyśmy sytuacji, w której nie wiemy, które testy zepsuły się, bo refaktoring rozbił beton, a które zepsuły się, bo niechcący popsuliśmy wymagania.
  • Testujemy wymagania, czyli to co robi system, a nie to w jaki sposób to robi. Testy pojawiają się, gdy pojawi się wymaganie.

Ale z drugiej strony - skoro piszesz testy implementacji, by ją "rozpoznać ogniem" i zrozumieć, co się dzieje pod spodem, to dlaczego masz je usuwać, gdy już zrozumiesz co się dzieje? Po Tobie przyjdzie następna osoba, która też będzie chciała zrozumieć i będzie znów pisać te testy. W tej sytuacji takie testy można by wręcz potraktować jak "żywą" dokumentację kodu. Jeśli implementacja się zmieni, należy odświeżyć dokumentację i tyle.

Zresztą, idąc dalej tym tokiem rozumowania:

  • Jeśli biznes nie napisze wprost w wymaganiach, jaki ma być performance / throughput systemu, to nie robimy performance / stress testów - nawet żeby wiedzieć, na czym stoimy i czy performance jest nazwijmy to "w granicach rozsądku"
  • Jeśli nie ma wymagania, by system był odporny np. na SQL injection, to możemy śmiało sklejać zapytania z user inputu i pchać do DB bez jakichś prepared statements i innych wymysłów które są szczegółem implementacyjnym
  • Jeśli nie ma wymagania, by system był rozszerzalny, utrzymywalny i testowalny oraz był tworzony zgodnie z dobrymi praktykami, to szkoda tracić czasu na code review, stosowanie statycznej analizy kodu (tym bardziej, że to testowanie implementacji do kwadratu) ani tym bardziej refaktoringiem - więc właściwie nieważne, czy kod jest zabetonowany, czy też nie

Nie mówiąc o tym, że... jak tu jednocześnie pisać testy, które można odpalić i błyskawicznie dostać feedback czy przechodzą i które klęknęły (tu bodajże Uncle Bob bardzo uroczo zilustrował onomatopejami takie testy, ale nie chce mi się szukać), a jednocześnie takie, które nie dotykają szczegółów implementacyjnych i sprawdzają całościowo czy system/moduł działa jak na leży.... i tak dalej i tak dalej? ;)

0

Na wstępie chciałbym powiedzieć, że staż mam raczej mały oraz w teamie za bardzo nie mam kogo się doradzić - nikt też nie testuje (w pewnym momencie chciałem pisać testy, ale każdy to olał) - a chciałbym zadać dodatkowe pytania.

Przed próbą pisania testów przeczytałem sporo artykułów na internecie - większa część była o testowaniu interfejsów oraz nietestowaniu prywatnych metod. Zastanawiam się, jeżeli testujemy interfejs który przetwarza w dość skomplikowany sposób dane mogąc zwrócić n różnych rodzajów błędów, to matematycznie możliwych scenariuszy edge-case'ów będzie 2^n. W jaki sposób powinny być pisane takie testy? Czy w przypadku gdy w odpowiedzi dostaniemy jakiś błąd walidacji, czy w teście powinno się zwracać uwagę na to jakiego rodzaju jest błąd - jeżeli tak, to musimy znać implementację (ze względu na kolejność walidacji).

8

@superdurszlak: a czytałeś klasyków IT?

Aisekai napisał(a):

Czy w przypadku gdy w odpowiedzi dostaniemy jakiś błąd walidacji, czy w teście powinno się zwracać uwagę na to jakiego rodzaju jest błąd - jeżeli tak, to musimy znać implementację (ze względu na kolejność walidacji).

Nie, nie musisz znać implementacji, musisz znać wymagania, że dla takiego zestawu danych, mają zostać zwrócone dane w konkretnej kolejności. Potem przepisujesz te wymagania na kod testów, a potem możesz napisać implementację.

4

to dlaczego masz je usuwać, gdy już zrozumiesz co się dzieje? Po Tobie przyjdzie następna osoba, która też będzie chciała zrozumieć i będzie znów pisać te testy.

Ale przecież właśnie zrozumienie systemu opiera sie o napisanie tych testów a nie o ich istnienie. To "droga jest celem" :)

W tej sytuacji takie testy można by wręcz potraktować jak "żywą" dokumentację kodu. Jeśli implementacja się zmieni, należy odświeżyć dokumentację i tyle.

Jeśli takie testy nadal masz, takie jakieś ultra-whitebox, to betonuja ci kod, bo dowolna zmiana je wywala. Jeśli ich nie uruchamiasz, zeby uniknąć tego problemu, to nikt ich nie aktualizuje. W efekcie nigdy się nie przydają.

Jeśli biznes nie napisze wprost w wymaganiach, jaki ma być performance / throughput systemu, to nie robimy performance / stress testów - nawet żeby wiedzieć, na czym stoimy i czy performance jest nazwijmy to "w granicach rozsądku"

To akurat zupełnie normalne.

Jeśli nie ma wymagania, by system był odporny np. na SQL injection, to możemy śmiało sklejać zapytania z user inputu i pchać do DB bez jakichś prepared statements i innych wymysłów które są szczegółem implementacyjnym

Bezpieczeństwo systemu jest raczej standardowym wymaganiem pozafunkcjonalnym i z góry zakłada się że jest brane pod uwagę. Podobnie jak np. latency w granicach rozsądku.

Jeśli nie ma wymagania, by system był rozszerzalny, utrzymywalny i testowalny oraz był tworzony zgodnie z dobrymi praktykami, to szkoda tracić czasu na code review, stosowanie statycznej analizy kodu (tym bardziej, że to testowanie implementacji do kwadratu) ani tym bardziej refaktoringiem - więc właściwie nieważne, czy kod jest zabetonowany, czy też nie

I znów bingo. Tylko ze zwykle to TY masz go potem rozwijać i ci za to zapłacą, więc dobre praktyki robisz dla siebie ;)

A tak generalnie o testach: nie ma panaceum. Nie ma czegoś takiego jak jakaś jedna poprawna droga. W jednym systemie sprawdzi sie X w innym Y. Wczoraj z technologiami jakie były sprawdziało sie jedno, a dzis sprawdza sie drugie. Np. kilka lat temu nie było dockera, nie było testcontainers, nie dało się ad-hoc momentalnie odpalic w teście różnych embedded komponentów żeby przeprowadzić testy integracyjne. Dzis juz się da.
W starych książkach można przeczytać np. o mockowaniu połączenia z bazą danych, ale czemu tak pisali? Bo byli głupi? Nie! Bo nie było takich możliwości żeby w teście szybko odpalić sobie bazę i przygotować pod test. Ale dziś juz takie możliwości są.

Niemniej obawiam się ze nie ma prostej odpowiedzi na postawione w tym wątku pytanie. Odpowiedzią jest zdrowy rozsądek.

1
Shalom napisał(a):

Np. kilka lat temu nie było dockera, nie było testcontainers, nie dało się ad-hoc momentalnie odpalic w teście różnych embedded komponentów żeby przeprowadzić testy integracyjne. Dzis juz się da.
W starych książkach można przeczytać np. o mockowaniu połączenia z bazą danych, ale czemu tak pisali? Bo byli głupi? Nie! Bo nie było takich możliwości żeby w teście szybko odpalić sobie bazę i przygotować pod test. Ale dziś juz takie możliwości są.

No to takie pytanie praktyczne, żeby się upewnić, co sugerujesz. Weźmy tę prezentację:
W skrócie chodzi o to, że gość refaktoryzuje z anemicznego modelu na jakiś bardziej domenowy. Nie robię w DDD/podobnych, to nie wiem, czy tak to się naprawdę robi, więc pytam.

Pierwsza wersja to transaction script z czymś takim (11:59):

class CompanyService{
	public async Task ImportCompany(string name){
		if(await _companiesContext.Companies.AnyAsync(x => x.Name == name)){ // Tu jest po prostu strzał do bazy SQL przykryty "ładnym" ORM-em
			throw new BusinessException("Company name must be unique");
		}
		
		var company = new Company();
		company.Name = name;
		
		_companiesContext.Add(company);
		await _companiesContext.SaveChangesAsync();
	}
}

Samo Company to POCO. Pomijam podkreślniki (tak niektórzy piszą w C#) i uwagi odnośnie stylu kodu, to nie jest tu istotne. Gość wprowadza statyczną metodę do tworzenia instancji Company i przenosi warunki do konstruktora, problematyczny jest tutaj strzał do bazy w momencie tworzenia obiektu, więc w efekcie kończy z czymś takim (26:29):

public interface ICompaniesCounter{
	int CountCompaniesWithName(string name);
}

oraz (28:07):

public class Company{
	private Company(string name, ICompaniesCounter companiesCounter){
		this.SetName(name, companiesCounter);
	}
	
	public static Company CreateImported(string name, ICompaniesCounter companiesCounter){
		return new Company(name, companiesCounter);
	}
	
	public void SetName(string name, ICompaniesCounter companiesCounter){
		if(companiesCounter.CountComapniesWithName(name) > 0){
			throw new BusinessException("Company name must be unique");
		}
		
		this.Name = name;
	}
}

Do tego oczywiście jakaś implementacja interfejsu strzelająca po bazie.

Dygresja: Tutaj fajnie widać, jak async/await w C# wszystko psuje, bo przedtem gość strzelał do bazy metodami Async, ale teraz tak nie może zrobić, bo nie da się zwrócić taska z konstruktora, więc strzela metodami blokującymi wątek.

No i teraz pytanie — jak to testować?

Z jednej strony @Shalom mówisz tak, jakbym miał postawić bazę SQL w kontenerze i odpowiednio zasilać ją danymi, aby nie używać tutaj mocka. No ale jak wtedy zmieni się logika walidacji (czyli zamiast sprawdzania tylko imienia będę też sprawdzał skróty, kolejność, przedrostki i cokolwiek innego), to nagle okaże się, że moje testy wywalają się z głupiego powodu, bo w testach związanych z Company pewnie nie zawsze obchodzi mnie poprawność, albo jeszcze gorzej — dawniej przechodziło, a teraz już nie, bo zmieniła się logika walidacji, a moje testy używają jakichś wartości z powietrza, które nie mają znaczenia dla kwintesencji testu, ale przypadkiem ją wywalają.

Z drugiej strony, mogę postawić mocka tego ICompaniesCounter i zwracać wartości, jakie mi trzeba. Domena jest "odporna" na zmiany w walidatorze, a znowu jego mogę przetestować w jakiś inny sposób (oczywiście ICompaniesCounter tu może być zwykłym strzałem do bazy, a może być gazylionem warunków i wywołań mikroserwisów). No ale wtedy też ryzykuję, że pracuję z obiektami niepoprawnymi biznesowo (bo nazwa się nie zgadza czy coś).

No i pytanie bonusowe: czy w ogóle umieszczenie interfejsu w konstruktorze takiego obiektu ma sens? Wyobrażam sobie, że potem będę musiał testować logikę domeny i instancje Company będą wszędzie, a ja nie chcę na każdym kroku męczyć się z tym interfejsem. Co wtedy? Dodać jakąś metodkę tworzącą obiekt na potrzeby testów, gdzie będę mógł ukryć/olać ten interfejs?

1

Zresztą, idąc dalej tym tokiem rozumowania:

  • Jeśli biznes nie napisze wprost w wymaganiach, jaki ma być performance / throughput systemu, to nie robimy performance / stress testów - nawet żeby wiedzieć, na czym stoimy i czy performance jest nazwijmy to "w granicach rozsądku"

Jak biznes napisze Ci wymagania wydajnościowe to będzie to coś w stylu "ma działać szybko, czego nie rozumiesz?", wiec na jedno wychodzi ;)

  • Jeśli nie ma wymagania, by system był odporny np. na SQL injection, to możemy śmiało sklejać zapytania z user inputu i pchać do DB bez jakichś prepared statements i innych wymysłów które są szczegółem implementacyjnym
  • Jeśli nie ma wymagania, by system był rozszerzalny, utrzymywalny i testowalny oraz był tworzony zgodnie z dobrymi praktykami, to szkoda tracić czasu na code review, stosowanie statycznej analizy kodu (tym bardziej, że to testowanie implementacji do kwadratu) ani tym bardziej refaktoringiem - więc właściwie nieważne, czy kod jest zabetonowany, czy też nie

Są takie pojęcia jak weryfikacja i walidacja, więc oprócz sprawdzenia czy produkt jest prawidłowy to sprawdzamy, czy jest tworzony prawidłowo :)

Nie mówiąc o tym, że... jak tu jednocześnie pisać testy, które można odpalić i błyskawicznie dostać feedback czy przechodzą i które klęknęły (tu bodajże Uncle Bob bardzo uroczo zilustrował onomatopejami takie testy, ale nie chce mi się szukać), a jednocześnie takie, które nie dotykają szczegółów implementacyjnych i sprawdzają całościowo czy system/moduł działa jak na leży.... i tak dalej i tak dalej? ;)

Dlatego są różne poziomy testów, a nawet możliwość uruchamiania innych zestawów testów w zależności od potrzeb. Nie musisz sprawdzać generowania raportu na temat wynagrodzeń w firmie minutę po tym, jak zmieniłeś coś w logowaniu użytkownika ;)

0
Afish napisał(a):

Z drugiej strony, mogę postawić mocka tego ICompaniesCounter i zwracać wartości, jakie mi trzeba. Domena jest "odporna" na zmiany w walidatorze, a znowu jego mogę przetestować w jakiś inny sposób (oczywiście ICompaniesCounter tu może być zwykłym strzałem do bazy, a może być gazylionem warunków i wywołań mikroserwisów). No ale wtedy też ryzykuję, że pracuję z obiektami niepoprawnymi biznesowo (bo nazwa się nie zgadza czy coś).

No taka chyba idea, że przy okazji stosowania DDD domena jest łatwo testowalna jednostkowo. Ta możliwość szybkiego stawiania bazy to raczej zaleta nie przy DDD lecz do całościowego testowania w przypadku podejść bardziej jak transaction script, gdzie nie możesz łatwo mockować, więc unit testami nie pokryjesz wszystkiego.

No i pytanie bonusowe: czy w ogóle umieszczenie interfejsu w konstruktorze takiego obiektu ma sens? Wyobrażam sobie, że potem będę musiał testować logikę domeny i instancje Company będą wszędzie, a ja nie chcę na każdym kroku męczyć się z tym interfejsem. Co wtedy? Dodać jakąś metodkę tworzącą obiekt na potrzeby testów, gdzie będę mógł ukryć/olać ten interfejs?

Ja osobiście nie lubię takiego designu, no ale w DDD tak chyba trzeba (chociaż to pewnie zależy od koloru książki, którą się przeczyta).
A, żeby się nie męczyć, to może lepiej zaprząc do tego Autofixture, które automatycznie zamockuje zależności i się nie przejmować?

1

@Afish:

postawić bazę SQL w kontenerze i odpowiednio zasilać ją danymi, aby nie używać tutaj mocka. No ale jak wtedy zmieni się logika walidacji (czyli zamiast sprawdzania tylko imienia będę też sprawdzał skróty, kolejność, przedrostki i cokolwiek innego), to nagle okaże się, że moje testy wywalają się z głupiego powodu, bo w testach związanych z Company pewnie nie zawsze obchodzi mnie poprawność, albo jeszcze gorzej — dawniej przechodziło, a teraz już nie, bo zmieniła się logika walidacji, a moje testy używają jakichś wartości z powietrza, które nie mają znaczenia dla kwintesencji testu, ale przypadkiem ją wywalają.

Ja rozwiązuje ten problem mając fluent buildery / generatory danych testowych, które zawsze są poprawne (i zmienialne explicite w teście). Więc jak zrobię:

        TestConfiguration testConfiguration = testHelper
                .createNewConfiguration()
                .withRandomUser()
                .withRandomFile()
                .setup()

I mam przygotowaną poprawną konfiguracje. A mogę zrobić:

        TestConfiguration testConfiguration = testHelper
                .createNewConfiguration()
                .withRandomUser()
                .withFile(TestFile.builder()
                        .withXYZ(xyz)
                        .build())

I teraz mam konfiguracje z tym jednym zmienionym polem.

Więc jak robię sobie testy tylko Company, to "dotykam" tylko wartości związanych z Company, a builder dba o to żeby wszystko inne było poprawne.

Przy czym ja nie twierdzę że testów jednostkowych nie należy robić. Jeśli masz złożoną logikę gdzieś daleko od entry-pointu aplikacji i chcesz ja testować w izolacji, to tak rób. Zdrowy rozsądek przede wszystkim! Odnosiłem się tylko do tego, że na pewne praktyki które można znaleźć w różnych książkach / poradnikach trzeba patrzeć przez pryzmat dostępnej technologii. Jeszcze kilka lat temu jakbyś powiedział że chcesz w testach stawiać N serwerów http i M różnych baz danych, to patrzeliby na ciebie jak na ufoludka i wrzucili w kaftan bezpieczeństwa ;) Do dziś niektórzy bezmyślnie powtarzają zasłyszane mity o tym że takie testy trwają strasznie długo i nie da się ich wygodnie używać, mimo że sami nigdy w życiu takiego na oczy nie widzieli...
Weź też pod uwagę, że testy integracyjne, takie ze stawianiem bazy i stukaniem w entry pointy aplikacji, testują całą "funkcje systemu". Więc możliwe że testy związane z Company w ogóle takimi testami nie będą, bo są jakimś wycinkiem domeny.

1

@Shalom wiadomo, że popłynąłem z tym security czy stosowaniem dobrych praktyk. Ale zrobiłem to z premedytacją - jak ktoś mi mówi, że coś jest dobre i super, to zastanawiam się ok, a co jeśli popchniemy to do ekstremum?, jakkolwiek absurdalne to ekstremum nie będzie.

Tym bardziej, że - przynajmniej takie odnoszę wrażenie - jak już raz stwierdzi się "to jest dobre" i obierze jakiś kierunek, to kod lubi potem powoli, powoli zbiegać sobie do tego ekstremum. Niby każdy wie, niby każdy rozumie, niby każdy się stara, niby coś tam - a prędzej czy później natrafiasz na jakiś miejmy nadzieję niewielki kawałek, gdzie wybiło poza skalę i zastanawiasz się, co poszło nie tak.

Jak z jakiegokolwiek powodu zaczniemy testować implementację - pewnego dnia okaże się, że jakiś kawałek jest zaspawany na amen :/

Jak będzie duże parcie na bycie high-level i nie ingerowanie w szczegóły - boję się sytuacji, gdzie to jakoś działa, ale nikt nie wie jak i przy każdej zmianie trzeba poznawać system od nowa, szczególnie gdy np. odejdzie ktoś, kto już go w miarę poznał. A jak do mikstury dodać jakieś enterprise kobyły, milion warstw, firmowe frameworki i inne cuda, to już w ogóle zrobi się dramat.

Żeby nie było, że wziąłem to podejście i hipotezę o dążeniu do ekstremów z powietrza - dokładnie to zaobserwowałem w kwestii nazewnictwa.

  • Tam, gdzie hołduje się długim, deskryptywnym nazwom, prędzej czy później nie mogę sobie nawet wyświetlić diffa side-by-side na jakimś pliku, bo się nie mieści. No i aż kusi, żeby przemianować, ale weź tu zmień nazwę na krótszą, gdy wszyscy chcą długich i deskryptywnych nazw.
  • Tam, gdzie ludzie mają w zwyczaju m.in. używać skrótów, prędzej czy później zaczną też pojawiać się skróty od skrótów, które potem ktoś dalej zastąpi literą i w pewnym momencie patrzysz na metodę wgzdtw która jest bardzo bardzo kluczowa dla świata, tylko trzeba ją odcyfrowywać przez kilka dni.

Niemniej obawiam się ze nie ma prostej odpowiedzi na postawione w tym wątku pytanie. Odpowiedzią jest zdrowy rozsądek.

Gdyby odpowiedź była prosta, nie byłoby o czym dyskutować ;)

Właśnie w tym rzecz - trzeba to robić rozsądnie, nie da się wszędzie tak samo, wszystko z głową, zależnie od sytuacji... no to jest prawda, ale z drugiej strony niby każdy teoretycznie to wie (o ile testuje), każdy się stara, raczej nikt nie sabotuje z premedytacją samego siebie - ale potem i tak wychodzi jak zwykle. Z ust ludzi, którzy pracują dużo dłużej niż ja czy Ty słyszałem, że nie widzieli jeszcze firmy, która od początku do końca testuje jak należy.

Szwedzi mają takie fajne określenie: Lagom

null null napisał(a):

Dlatego są różne poziomy testów, a nawet możliwość uruchamiania innych zestawów testów w zależności od potrzeb. Nie musisz sprawdzać generowania raportu na temat wynagrodzeń w firmie minutę po tym, jak zmieniłeś coś w logowaniu użytkownika ;)

Owszem, są, i wiadomo że niekoniecznie musisz odpalać wszystkie 100k testów żeby sprawdzić jedną małą zmianę. Ale jednak w niektórych środowiskach bywa, że nawet uruchomienie jednego testu może zająć zdecydowanie zbyt długo, by mówić o jakiejkolwiek płynności pracy. Inna kwestia, że jeśli jestem w stanie odpalać tylko jakiś mały podzbiór testów, by móc normalnie pracować, to już jest problem - tym bardziej, że to, że testy powinny być odizolowane od siebie nie oznacza, że rzeczywiście zawsze są - a tego typu kwiatki też chcemy wyłapać jak najwcześniej.

Poza tym - nie zrozum mnie źle, nie neguję ich wartości - istnienie wielu poziomów testów też może prowadzić do różnych "ciekawych" przypadków, o których pisałem w pierwszym poście ;)

Pojawia się też jeszcze jeden temat, którego nie poruszyłem - testowanie aplikacji opartej np. o framework DI, gdzie żonglowanie zależnościami niekoniecznie musi być trywialne, ale jest oddelegowane do frameworka. Wiadomo, że

  • można napisać testy niewymagające stawiania kontekstu frameworka DI, i poskładać wszystko samemu
  • powinniśmy testować aplikację a nie framework, a przecież prawidłowe postawienie kontekstu to w tej sytuacji zadanie frameworka
  • możemy założyć, że skoro testy high-level przechodzą to wszystko jest OK i tak zapewne jest

I tu pojawia się mały problem:

  • szybkie testy bez stawiania kontekstu aplikacji itp. nie wykryją np. zepsutej konfiguracji beanów. Czasem nawet nie tyle zepsutej, co źle napisanej i wrażliwej na zmiany - przez co podbicie wersji frameworka i zmiana jakiegoś wewnętrznego defaulta we frameworku wywala stawianie kontekstu.
  • testy "na bogato" będą pewnie dość wolne i upierdliwe w używaniu na bieżąco. Zresztą nawet jako część pipeline CI byłyby uciążliwe - kto by chciał czekać pół godziny, aż testy na branchu przejdą...
  • można próbować postawić mały kawałek kontekstu (przynajmniej w niektórych frameworkach) zamiast całości i testować taki kawałek, ale to już zakrawa na wcinanie się w implementację. Nie mówiąc o tym, że ww. problemy z zepsutą konfiguracją mogą się objawić dopiero przy stawianiu całego kontekstu, i tak dalej i tak dalej...
0

Jak będzie duże parcie na bycie high-level i nie ingerowanie w szczegóły - boję się sytuacji, gdzie to jakoś działa, ale nikt nie wie jak i przy każdej zmianie trzeba poznawać system od nowa, szczególnie gdy np. odejdzie ktoś, kto już go w miarę poznał. A jak do mikstury dodać jakieś enterprise kobyły, milion warstw, firmowe frameworki i inne cuda, to już w ogóle zrobi się dramat.

Samo życie :D Potem wrzucasz to na dedykowany serwer, barykadujesz drzwi od serwerowni i stawiasz adapter i zapominasz w ogóle o tej aplikacji :D

Z ust ludzi, którzy pracują dużo dłużej niż ja czy Ty słyszałem, że nie widzieli jeszcze firmy, która od początku do końca testuje jak należy

Ja taką widziałem -> Codewise.

testy "na bogato" będą pewnie dość wolne i upierdliwe w używaniu na bieżąco. Zresztą nawet jako część pipeline CI byłyby uciążliwe - kto by chciał czekać pół godziny, aż testy na branchu przejdą...

Mitologia, chyba że masz monolit na miliony linii kodu, który ma tysiące różnych endpointów i funkcji do przetestowania. Żeby nie być gołosłownym, jakiś losowy serwis który mam pod ręką, ~60 testów integracyjnych, czas wykonania 3.5s. Nie czuje wielkiej uciążliwości, ale wiem że dla niektórych te sekundy są na wagę złota ;)

1

próbuję dojść do jakiegoś (choćby pozornie) jednoznacznego wniosku, jak tak naprawdę należy testować...

Ciekawe odpowiedzi uzyskasz, gdy zwrócisz uwagę na ludzkę naturę, bo testy są bardziej z nią związane niż z dowodami matematycznymi.

Gdy odwrócisz pytanie i zamiast zastanawiać się jak testować, zadasz sobie pytanie dlaczego piszesz testy.

Czy chodzi Ci o to, by objąć całą złożoność projektu? Chcesz mieć to wszystko pod nieograniczoną kontrolą? Chcesz mieć kod odporny na zmiany?

Jeśli odpowiedź to tak to błądzisz i nawet nie wiesz jak bardzo. Właśnie próbujesz swoją niekompetencję przykryć grubą warstwą testów.

Testy wyłączają myślenie.

Zamiast rozkminiać nie wiadomo jak złożone techniki testowania, przeznacz swój czas na myślenie o tym co ważne.

Jesli wypracujesz lepsze prostsze rozwiązanie, wtedy będzie ono mniej zawiłe, nie będzie wymagało szerokiego zestawu skomplikowanych testów.

Testy to tylko kilka przypadków, nie próbuj na nich opierać swojego rozwiązania, nie próbuj przez nie komplikować swojego rozwiązania.

Postaw na myślenie, i na dostrzeganie rzeczy jakie coraz słabiej widzisz - wtedy nie strzelisz sobie w kolano.

1
Shalom napisał(a):

Samo życie :D Potem wrzucasz to na dedykowany serwer, barykadujesz drzwi od serwerowni i stawiasz adapter i zapominasz w ogóle o tej aplikacji :D

Byłem, widziałem, a potem ktoś stwierdza że fajnie by było zintegrować tego potworka z potworkiem third-party, za pośrednictwem trzeciego potworka... i wszystko poprzez DB w której trzy potworki orają bezpośrednio. Co może pójść źle... no, ale 'murican engineer postanowił, a polaczki miały tylko naklepać co guru zarządził. Wszystko testowalne aż miło...

Z ust ludzi, którzy pracują dużo dłużej niż ja czy Ty słyszałem, że nie widzieli jeszcze firmy, która od początku do końca testuje jak należy

Ja taką widziałem -> Codewise.

Mógłbyś rozwinąć?

testy "na bogato" będą pewnie dość wolne i upierdliwe [....]
Mitologia

Niestety nie zawsze, ale owszem przy milionowcach te objawy są najbardziej wyraziste ;)

Żeby nie być gołosłownym, jakiś losowy serwis który mam pod ręką, ~60 testów integracyjnych, czas wykonania 3.5s. Nie czuje wielkiej uciążliwości, ale wiem że dla niektórych te sekundy są na wagę złota ;)

Oszczędzaj RAM gdziekolwiek jesteś i te sprawy :]

Nie no, bez przesady. 3,5s czy nawet 15s na całą suitę to jest pierdnięcie. Ale nie zawsze ma się tyle szczęścia

  • jest sobie enterprise-korpo-kobyła, w niej pełno generowania, kompilowania, przebudowywania, wszystko zajmuje wieki. Potem całość kilka minut wstaje. Nie da się na tym sprawnie developować, wszystko trwa boleśnie długo.
  • jest ten serwis-potworek od integrowania z innymi, w nim paręset testów integracyjnych... i Flyway który przed każdym testem orze bazę, feeduje, katuje ilomaś migracjami. Kilka-kilkanaście sekund na jeden test, paręnaście minut na całość. Dramat. Wiadomo, to była tylko i wyłącznie nasza wina, sami się tak urządziliśmy i potem trzeba to było odkręcać, ale jednak.
  • jest sobie duży, duży kawał softu. Testy są relatywnie high-level, bo duży kawał softu bierze kod źródłowy w IR, kompiluje do asm i testy sprawdzają, czy wypluty asm jest poprawny i optymalny (przynajmniej na obecny stan wiedzy testującego). Samo wykonanie testu jest w miarę szybkie - na tyle szybkie, że testy lecą dla X różnych zestawów flag, optymalizacji, wykonuje się ich od czorta na tych wszystkich kombinacjach i da się żyć. Problem - przebudowanie tego ustrojstwa. Trwa wieczność.
semicolon napisał(a):

Zamiast rozkminiać nie wiadomo jak złożone techniki testowania [...] nie będzie wymagało szerokiego zestawu skomplikowanych testów [...] nie próbuj przez nie komplikować swojego rozwiązania.

Chyba troszkę za dużo sobie dopowiedziałeś. Nie szukam jakichś wymyślnych technik i rytuałów - przeciwnie, konieczność trzymania się magicznych rytuałów (odpalam tylko te 3 testy które to tykają, reszty nie albo odpalam tylko tę jedną warstwę testów, szkoda czasu na pozostałe etc.) jest samo w sobie upierdliwe. Szukam przeciwieństwa dla skomplikowania - prostej heurystyki, która nie wiedzie na manowce. Albo wiedzie wolniej niż inne.

Właśnie próbujesz swoją niekompetencję przykryć grubą warstwą testów.

Nie, w ukrywaniu niekompetencji nigdy nie byłem dobry ;)

Czy chodzi Ci o to, by objąć całą złożoność projektu? Chcesz mieć to wszystko pod nieograniczoną kontrolą? Chcesz mieć kod odporny na zmiany?

Chcę mieć podejście do testowania, które rozwiązując jeden problem nie spowoduje / nie pogłębi dwóch innych - ani dziś, ani za 3 lata

1

jest sobie enterprise-korpo-kobyła, w niej pełno generowania, kompilowania, przebudowywania, wszystko zajmuje wieki. Potem całość kilka minut wstaje. Nie da się na tym sprawnie developować, wszystko trwa boleśnie długo.

Rozumiesz ze problem nie leży w testach? ;) Czas wykonania testów to tutaj tylko skutek, a leczy się objawy a nie skutki ;) To trochę jak narzekanie, że code review nie są dobre, bo ty nie możesz robić w firmie code-review bo macie przypisanie 1 osoba na serwis/projekt i wszędzie single-point-of-failure i nikt nie może ci zrobić review, bo nikt oprócz ciebie nie zna kodu ;) Logika ta sama.

LLVM i w ogóle kompilatory/generatory kodu to trochę inna bajka, bo tam masz sytuacje odwrotną niż w większości popularnych systemów. Zamiast względnie prostej logiki, która potrzebuje komunikacji z innymi serwisami, masz bardzo złożoną logikę, która ora po samym tylko inpucie. Tutaj niestety e2e będzie czasochłonne i nie przeskoczysz tego. Można ofc napisać coś takiego na tyle modułowo, żeby dało się pracować/testować tylko kawałki.

Mógłbyś rozwinąć?

Pytałeś o firmę która w moim odczuciu testuje tak jak należy, więc taką firmę podałem :) Czy jest tam idealnie? Trudno powiedzieć, ale nie widziałem jeszcze lepszego rozwiązania. Niemniej jako się to kręci, jest continuous deployment, zmiany na proda idą praktycznie non-stop. Wszystkie serwisy są pokryte testami integracyjnymi i e2e do tego stopnia, że wiesz, że jak się coś zbudowało i testy są zielone to możesz klikać deploy i będzie działać.

0
Afish napisał(a):
Shalom napisał(a):

Np. kilka lat temu nie było dockera, nie było testcontainers, nie dało się ad-hoc momentalnie odpalic w teście różnych embedded komponentów żeby przeprowadzić testy integracyjne. Dzis juz się da.
W starych książkach można przeczytać np. o mockowaniu połączenia z bazą danych, ale czemu tak pisali? Bo byli głupi? Nie! Bo nie było takich możliwości żeby w teście szybko odpalić sobie bazę i przygotować pod test. Ale dziś juz takie możliwości są.

No to takie pytanie praktyczne, żeby się upewnić, co sugerujesz. Weźmy tę prezentację:
W skrócie chodzi o to, że gość refaktoryzuje z anemicznego modelu na jakiś bardziej domenowy. Nie robię w DDD/podobnych, to nie wiem, czy tak to się naprawdę robi, więc pytam.

Pierwsza wersja to transaction script z czymś takim (11:59):

class CompanyService{
	public async Task ImportCompany(string name){
		if(await _companiesContext.Companies.AnyAsync(x => x.Name == name)){ // Tu jest po prostu strzał do bazy SQL przykryty "ładnym" ORM-em
			throw new BusinessException("Company name must be unique");
		}
		
		var company = new Company();
		company.Name = name;
		
		_companiesContext.Add(company);
		await _companiesContext.SaveChangesAsync();
	}
}

Samo Company to POCO. Pomijam podkreślniki (tak niektórzy piszą w C#) i uwagi odnośnie stylu kodu, to nie jest tu istotne. Gość wprowadza statyczną metodę do tworzenia instancji Company i przenosi warunki do konstruktora, problematyczny jest tutaj strzał do bazy w momencie tworzenia obiektu, więc w efekcie kończy z czymś takim (26:29):

public interface ICompaniesCounter{
	int CountCompaniesWithName(string name);
}

oraz (28:07):

public class Company{
	private Company(string name, ICompaniesCounter companiesCounter){
		this.SetName(name, companiesCounter);
	}
	
	public static Company CreateImported(string name, ICompaniesCounter companiesCounter){
		return new Company(name, companiesCounter);
	}
	
	public void SetName(string name, ICompaniesCounter companiesCounter){
		if(companiesCounter.CountComapniesWithName(name) > 0){
			throw new BusinessException("Company name must be unique");
		}
		
		this.Name = name;
	}
}

Do tego oczywiście jakaś implementacja interfejsu strzelająca po bazie.

Dygresja: Tutaj fajnie widać, jak async/await w C# wszystko psuje, bo przedtem gość strzelał do bazy metodami Async, ale teraz tak nie może zrobić, bo nie da się zwrócić taska z konstruktora, więc strzela metodami blokującymi wątek.

No i teraz pytanie — jak to testować?

Z jednej strony @Shalom mówisz tak, jakbym miał postawić bazę SQL w kontenerze i odpowiednio zasilać ją danymi, aby nie używać tutaj mocka. No ale jak wtedy zmieni się logika walidacji (czyli zamiast sprawdzania tylko imienia będę też sprawdzał skróty, kolejność, przedrostki i cokolwiek innego), to nagle okaże się, że moje testy wywalają się z głupiego powodu, bo w testach związanych z Company pewnie nie zawsze obchodzi mnie poprawność, albo jeszcze gorzej — dawniej przechodziło, a teraz już nie, bo zmieniła się logika walidacji, a moje testy używają jakichś wartości z powietrza, które nie mają znaczenia dla kwintesencji testu, ale przypadkiem ją wywalają.

Z drugiej strony, mogę postawić mocka tego ICompaniesCounter i zwracać wartości, jakie mi trzeba. Domena jest "odporna" na zmiany w walidatorze, a znowu jego mogę przetestować w jakiś inny sposób (oczywiście ICompaniesCounter tu może być zwykłym strzałem do bazy, a może być gazylionem warunków i wywołań mikroserwisów). No ale wtedy też ryzykuję, że pracuję z obiektami niepoprawnymi biznesowo (bo nazwa się nie zgadza czy coś).

No i pytanie bonusowe: czy w ogóle umieszczenie interfejsu w konstruktorze takiego obiektu ma sens? Wyobrażam sobie, że potem będę musiał testować logikę domeny i instancje Company będą wszędzie, a ja nie chcę na każdym kroku męczyć się z tym interfejsem. Co wtedy? Dodać jakąś metodkę tworzącą obiekt na potrzeby testów, gdzie będę mógł ukryć/olać ten interfejs?

Jak dla mnie to co przedstawia film to jest własnie strzelanie sobie w stopę od strony testów.

    public void SetName(string name, ICompaniesCounter companiesCounter){
        if(companiesCounter.CountComapniesWithName(name) > 0){
            throw new BusinessException("Company name must be unique");
        }

        this.Name = name;
    }

Lepiej jest przekazać int CompaniesNumber wtedy nie trzeba nic mockować, co zmniejszy ilość kodu w testach.
DDD namawia do takiego injectowanie serwisów w metody ale nie w takim kontekście, tutaj to nie ma sensu. Intefejsy są przereklamowane w testowaniu, w większości języków można zapewnić testowanie bez intefejsów w ogóle .

No ale jak wtedy zmieni się logika walidacji (czyli zamiast sprawdzania tylko imienia będę też sprawdzał skróty, kolejność, przedrostki i cokolwiek innego), to nagle okaże się, że moje testy wywalają się z głupiego powodu, bo w testach związanych z Company pewnie nie zawsze obchodzi mnie poprawność, albo jeszcze gorzej — dawniej przechodziło, a teraz już nie, bo zmieniła się logika walidacji, a moje testy używają jakichś wartości z powietrza, które nie mają znaczenia dla kwintesencji testu, ale przypadkiem ją wywalają.

Assercja, Design By Contract, Refined type ze Scali czy Haskella to oczywiście forma walidacji ale to zupełnie inny koncept niż walidacja dla użytkownika interfejsu graficznego gdzie chodzi o interakcje użytkownika z UI. Assercja, Design By Contract, Refined to informacja dla Deva to takie defensywne programowanie. Wpychanie walidacji pod UI w model dziedziny to tak nie za bardzo.

Jeśli używasz assercji to raczej nic nie musisz testować assercja jest sama w sobie testem, to samo z Refined type.

Jeśli w testach spójność modelu nie jest istotna to znaczy, że masz źle napisany model i ogólnie coś poszło nie tak.

0

Chcę mieć podejście do testowania, które rozwiązując jeden problem nie spowoduje / nie pogłębi dwóch innych - ani dziś, ani za 3 lata

Po prostu testuj najbardziej kluczowe rzeczy z rdzenia, reszta jest stokroć mniej istotna.

Jak na stronie nie pokaże się kolorowy guzik, to pech, wgracie go parę godzin później.

11

Luźno zlepek moich zasad, które od jakiegoś czasu stosuję i w sumie działa (przestałem na testy narzekać). W różnych zespołach pracuje, ale jakoś się na temat tych zasad nie kłócimy (mamy gorące dyskusje w innych miejscach).

  1. Testy to testy - żadnych głupich podziałów na unitowe, komponentowe, integracyjne itp.
  2. Żadnych junitów. Kotest, scalatest to podstawa (w tym kotest do projektów javowych) - kiedyś byłby spock, ale dla mnie czas spoka już minął
    W kotest stosuję notację given / when / then - ale akurat nie jestem w pełni z tego stylu zadowolony - ciągle eksperymentuje.
    ( Tu może wyjaśnienie - nie lubię bdd tak jak w kotest - bo mam powtórzenia - teskty z then powtarzają się w asercji (DSL). Często trudno jest sformułować given.
Given (" all that that you need  for this test ") { //ale berło
... // bo tu inicjuje kilka rzeczy - 3 -4 linijki - wszystkie są potrzebne
...
 .    When ("kiełbasa się zagotuje") {
...
     Then("result is false") {
        result should be (false) // normalnie zaskoczenie
    }
  }
}
  1. Żadnych mockitów i innych cudów - testujemy nasz kod a nie mockito. Jak jest coś w stylu mocka potrzebne, to piszemy to ręcznie - paradoks z kilku projektów jest taki, że dzięki temu mamy mniej kodu...
    (Tak dało się porównać, bo kiedyś jeden systemik został zdemockitowany (kolega się wkurzył na wiecznie zielone testy) )

  2. Testy całkowicie izolowane od obcych baz danych systemów, czasu dnia itd. Niby oczywiste, ale tu większośc teamów ma coś na sumieniu.

  3. Mierzymy pokrycie, ale traktujemy to swobodnie - wzrosło to dobrze, spadło - no to zobaczmy co nie jest pokryte - jak możemy z tym żyć to idziemy dalej.

  4. Jak wyjdzie bug na produkcji (albo na testach ręcznych) to wtedy na pewno zaczynamy od pisania testu.

  5. Jak mamy bazę danych, to w testach bazy danych używamy - z tym, że tu leci zwykle in memory h2 - która całkiem nieźle udaje oracle. Baza danych jest całkowicie inicjalizowana ze skryptów - pełne wersjonowanie.

  6. Przeważnie stosuje TDD, ale tu jestem raczej sam. nie chroni mnie to przed błędami (stosuję zasadę złośliwego TDD - robić minimalny kod spełniający test - już niejedna absurdalna implementacja wpadła tak na produkcję.

  7. Jak robimy serwis rest to na koniec pewnie będą testy które dla danego HTTP oczekują jakiegos tam wyniku - ktoś by powiedział e2e, ktoś inny, że integracyjne - dla mnie to testy nie różnią się niczym od innych testów (są w innym pakiecie - tam gdzie serwis rest - zwany często z durna kontrolerem).

  8. Oczywiście żadnych spy i testowania czy metoda była wywołana, jeśli testowany kod potrafi zwrócić oczekiwane wyniki bez wyciągania czegokolwiek z repo. innych serwisów itp. - to tym lepiej dla kodu.

  9. Jak wywołujemy serwisy restowe inne to można użyć np. wiremock, ale można też ręcznie napisać symulator takiego serwisu (czasem się przydaje, dużo nie kosztuje).

  10. Jak piszę test to staram się go zobaczyć chociaż raz na czerwono. Jeśli test nie był napisany przed kodem - to wtedy stashuje /wywalam kod i odpalam test.

  11. Czasem wywalam testy - w dwóch przypadkach:
    a) testy są zawsze zielone
    b) testy są zawsze czerwone

  12. Zasadniczo nie mockuje (nawet ręcznymi mockami) klas z tego samego modułu, projektu. Kiedyś ta zasada brzmiała: nie mockuje klas powstałym w tym samym pokoju, w którym siedze (w którym siedzi zespół).

  13. Mega ważne - prawie byłbym zapomniał - nigdy nie testuje: klas, metod itp. Testuje potrzebną funkcjonalność - która akurat w jezykach na jvm jest zwykle ukryta pod metodami i leży w klasach. To tylko zmiana nastawienia - filozoficzna, ale IMO ma duży wpływ.

  14. Nie testuję lomboka, getterów, setterów - bo nie używam ani lomboka, ani getterów ani setterów.

  15. Nie testuje metod zwracających void - usuwam te metody - nic nie robią to nie szkoda.
    ( ostatnie 2 punkty - w kotlinie prosto - w javie są czasem kontrowersje w zespole).

Z czego jestem niezadowolony - z frontu.
Sam JS (logika) jest super testowalny - fajne frameworki.
CSS nie umiem testować, a czasem gdzies tam się layout rypnie ...
Selenium ... za drogie w utrzymaniu. Taniej wychodzi jak raz na jakiś czas prodcyjne stronki rypną. Oczywiście tu zależy od skali - ile kosztuje przestój na takiej produkcji i jak łatwo jest naprawiać.

0
jarekr000000 napisał(a):
  1. Jak mamy bazę danych, to w testach bazy danych używamy - z tym, że tu leci zwykle in memory h2 - która całkiem nieźle udaje oracle. Baza danych jest całkowicie inicjalizowana ze skryptów - pełne wersjonowanie.

@jarekr000000
Fajny post, pierwszy raz słyszę o kotest. Pytanie do w.w. podpunktu - bazę danych stawiacie już z jakimiś danymi czy pustą?

2

@somekind

somekind napisał(a):

A, żeby się nie męczyć, to może lepiej zaprząc do tego Autofixture, które automatycznie zamockuje zależności i się nie przejmować?

A jak to rozwiąże problem wtedy też ryzykuję, że pracuję z obiektami niepoprawnymi biznesowo (bo nazwa się nie zgadza czy coś)? Autofixture wypluje mi losowe wartości, więc dostaję coś, co walidacji przejść nie powinno, to koncepcyjnie nie różni się od schowania Company za metodą wstrzykującą mocka walidatora przepuszczającego każdy obiekt. Oczywiście oszczędzam na liniach kodu i gdzie mogę, tam używam Autofixture, ale tutaj nie jestem przekonany.

@Shalom

Shalom napisał(a):

Ja rozwiązuje ten problem mając fluent buildery / generatory danych testowych, które zawsze są poprawne (i zmienialne explicite w teście). Więc jak zrobię:

        TestConfiguration testConfiguration = testHelper
                .createNewConfiguration()
                .withRandomUser()
                .withRandomFile()
                .setup()

I mam przygotowaną poprawną konfiguracje. A mogę zrobić:

        TestConfiguration testConfiguration = testHelper
                .createNewConfiguration()
                .withRandomUser()
                .withFile(TestFile.builder()
                        .withXYZ(xyz)
                        .build())

I teraz mam konfiguracje z tym jednym zmienionym polem.

Nie widzę, jak to odpowiada na moje pytanie. Ja wiem, że mogę sobie poukrywać tworzenie Company itp, ale tu pytanie, czy powinienem mockować walidację, czy ją zawsze odpalać. Te Twoje dane testowe by ją odpalały? Jak tak, to ktoś zmieni kod walidacji i nagle będzie wszystko na czerwono? Do tego zapewne to nie będzie „jedno Company” do zmiany, więc roboty będzie więcej, niż tylko w jednej linijce.

Ponadto z mojej praktyki takie podejście jest ryzykowne. Widziałem to wiele razy w projektach i potem kończy się tym, że strach to dotknąć, bo wszystkie testy używają tych samych danych, więc nagle wszystko się sypie. W efekcie ludzie robią osobne zestawy, żeby mieć izolację między testami. U Ciebie tak nie jest?

Do dziś niektórzy bezmyślnie powtarzają zasłyszane mity o tym że takie testy trwają strasznie długo i nie da się ich wygodnie używać, mimo że sami nigdy w życiu takiego na oczy nie widzieli...

Cóż, ja widziałem, jedna baza postawiona lokalnie, a testy działały przez prawie 10 godzin, więc nie mam zielonego pojęcia, jak Ty to robisz w 3 sekundy (oczywiście tamte testy były też mocno patologiczne, ale chodzi mi o to, że zejście do sekund jest dla mnie jakąś czarną magią, do minuty jeszcze bym uwierzył). Jarek jeszcze pisze o jakichś bazach in memory, to też mnie nie przekonuje, bo nieraz widziałem, jak na takiej bazie wszystko działa, a na prawdziwej się sypie (bo coś tam w SQL-u nie pasowało itp).

1

Co do baz in memory - to jeśli produkcja jest na oracle to h2 całkiem nieźle udaje (taki cel). Raz na rok (a może rzadziej) jest coś co na oracle inaczej działa.
Inna sprawa, że w ogóle staram się baz danych nie używać, więc może te problemy widzę nawet rzadziej (ale serio kojarze jedną akcję z zeszłego roku, gdzie akurat testy się wywaliły ( a na oracle by przeszło)).
EDIT:

Ćwiczyliśmy kilka razy z testami na oracle - stawianym np. z dockera (i to pod windows he he) - ale to było naprawdę wolne i upierdliwe (też fakt, że to akurat było robione na sprzęcię bogatego banku - czytaj: maszyny developerskie biedne, że aż wstyd).

1

Nie widzę, jak to odpowiada na moje pytanie. Ja wiem, że mogę sobie poukrywać tworzenie Company itp, ale tu pytanie, czy powinienem mockować walidację, czy ją zawsze odpalać. Te Twoje dane testowe by ją odpalały? Jak tak, to ktoś zmieni kod walidacji i nagle będzie wszystko na czerwono? Do tego zapewne to nie będzie „jedno Company” do zmiany, więc roboty będzie więcej, niż tylko w jednej linijce.

Ja mówie o testach które uderzają w entry point aplikacji, np. jakiś rest endpoint. Więc oczywiście że odpalą walidacje, bo to przecież jest normalne zachowanie aplikacji. Testujemy czy cała funkcja systemu dziala. Jak ktoś zmienił walidacje, to najwyżej muszę zmienić sobie jedną metodę, która generuje mi testowe obiekty Company.

Ponadto z mojej praktyki takie podejście jest ryzykowne. Widziałem to wiele razy w projektach i potem kończy się tym, że strach to dotknąć, bo wszystkie testy używają tych samych danych, więc nagle wszystko się sypie. W efekcie ludzie robią osobne zestawy, żeby mieć izolację między testami. U Ciebie tak nie jest?

Zupełnie nie bo ja nie mówie o wygenerowanych danych testowych/hardkodowanych gdzieś w kodzie. Ja mówie o losowych danych które są tworzone ad-hoc per test. Dodatkowo w jakimś @After usuwam wszystko z jakichś baz in-memory i resetuje wszystkie httpmocki. Nie da się ich nijak "reużyć" między testami. Ten kod który podałem wyżej generuje stan który sobie wybierzesz i tyle. To nie jest jakis hardkodowany stan.

Wyobraź sobie że masz TestCompanyFactory i w teście mówisz u daj mi losowe Company i dostajesz CompanyBuilder który już ma wszystko ustawione "poprawnie". Jak w teście chcesz coś zmienić bo sprawdzasz co się stanie jak masz poop emoji w company name, to zmieniasz to tym builderem i budujesz sobie obiekt do pracy w teście.

oczywiście tamte testy były też mocno patologiczne, ale chodzi mi o to, że zejście do sekund jest dla mnie jakąś czarną magią, do minuty jeszcze bym uwierzył

A co tu ma wstawać tyle godzin? Httpmocki/Wiremocki? H2/HSQL? Co tam jeszcze potrzebujesz? Jak koniecznie chcesz odpalić coś "ciężkiego" to bierzesz testcontainers które startuje ci Cassandre czy Oracle w dockerze. Trwa to chwilę dłużej, ale to na własne życzenie.

A narzekanie że H2 nie imituje ci dobrze twojej bazy jest śmieszne, skoro jako alternatywę podajesz mockowanie, które w ogóle niczego nie imituje :D

Fakt, bywają różnie między bazami, sam mam troche serwisów gdzie z properties czytam sobie nazwę metody która zwraca timestamp, czy np. nazwę tabeli (bo niektóre bazy mają jakieś śmieszne namespace), ale jeszcze nie widziałem sytuacji gdzie nie wystarczyło właśnie wyciągnąć czegoś takiego do property. Fakt, nie testujesz dokładnie tego samego co na produkcji, ale od tego masz testy e2e.

0
Shalom napisał(a):

Ja mówie o testach które uderzają w entry point aplikacji, np. jakiś rest endpoint. Więc oczywiście że odpalą walidacje, bo to przecież jest normalne zachowanie aplikacji. Testujemy czy cała funkcja systemu dziala. Jak ktoś zmienił walidacje, to najwyżej muszę zmienić sobie jedną metodę, która generuje mi testowe obiekty Company.

Okej, to mówisz o innych testach.

Wyobraź sobie że masz TestCompanyFactory i w teście mówisz u daj mi losowe Company i dostajesz CompanyBuilder który już ma wszystko ustawione "poprawnie". Jak w teście chcesz coś zmienić bo sprawdzasz co się stanie jak masz poop emoji w company name, to zmieniasz to tym builderem i budujesz sobie obiekt do pracy w teście.

Skoro ustawiony "poprawnie", to musi przejść walidację, czyli jak się zmieni jej logika, to trzeba zmienić builder. Ale jak to robisz do testów e2e, to nie jest problem, zgadzam się. Ja mówię o testach niższego poziomu, niekoniecznie "jednostkowych" w sensie klasy, ale też nie e2e. Takich testów pewnie będę miał setki w całej aplikacji.

Trwa to chwilę dłużej, ale to na własne życzenie.

No właśnie, tu "chwila dłużej", a potem robią się minuty i godziny.

A narzekanie że H2 nie imituje ci dobrze twojej bazy jest śmieszne, skoro jako alternatywę podajesz mockowanie, które w ogóle niczego nie imituje :D

Nie mówię o testach e2e, w nich w ogóle nie chcę mockować ani bawić się w bazy w pamięci, tylko strzelam do prawdziwego stosu. Znowu jeżeli Twoje testy e2e jadą na H2 (a nie produkcyjnej wersji bazy), to też lipa.

ale jeszcze nie widziałem sytuacji gdzie nie wystarczyło właśnie wyciągnąć czegoś takiego do property

No to ja widziałem wielokrotnie, jakieś przypadki, że ta sama baza działa inaczej na windowsie niż na innym systemie, że składnia się nie zgadza między wersjami, że nazwy domen psuły testy itp.

2
jarekr000000 napisał(a):

jeśli produkcja jest na oracle to h2 całkiem nieźle udaje (taki cel). Raz na rok (a może rzadziej) jest coś co na oracle inaczej działa.

Czyli rozumiem, że nawet w testach najwyższego poziomu - e2e, super integracyjnych, czy jak je sobie tam nazwiemy - nie używasz "produkcyjnego oracla" tylko "in memory h2"? To jak dla mnie to jest słabe, osobiście wolę mieć w chmurce wystawiony cały stos aplikacji i na nim przetestować integracyjnie, a nie z jakimiś atrapami (a czy do mock czy baza in memory, to nic nie zmienia, to ciągle nie jest ten produkt, którego mam na produkcji).

1

Nie używam produkcyjnego oracla w testach. Używałbym jakby było to szybkie i bezproblemowe.

Faktyczne produkcyjna baza, ( ba! INSTANCJA ) jest użyta tylko w monitoringu - gdzie niby są odpalane testy - (Selenium), ale to nie są testy, bo nie pokazują stanu źródeł tylko faktyczny stan działającej aplikacji. To jest monitoring.
Do tego jest ich (testów ) całe 2 (taki smoke).

Wystarcza i sprawdza się w różnych miejscach takie podejście.

Jakbym miał tak w chmurce uczciwie przetestować stos aplikacji - to by wyszło, że muszę postawić 20 instancji oracle, redisa, kilka sftp, kupe dziwnych softów i hardware związanych z szyfrowaniem, 100 kilka serwisów na tym i cholera wie jeszcze co.
Całkiem chwalebne, ale jestem jeszcze dość daleko od tego i nawet nie jest to w moich celach.
(Widziałem firme, która tak robi - klik - czekasz 4 godziny i masz zasymulowany rok pracy małego banku - sprawdzasz czy stany kont się zgadzają)

EDIT - pytanie.
A jak tego Oracle masz odpalanego na CI? Bo to jest generalnie problem (nawet nie wiem czy nie powoduje dodatkowych kosztów -licencja )

0
jarekr000000 napisał(a):

Nie używam produkcyjnego oracla w testach. Używałbym jakby było to szybkie i bezproblemowe.

No to mnie zmyliłeś. Piszesz, że nie mockujesz, tylko stawiasz bazę, ale tak naprawdę stawiasz bardzo mądrą atrapę w pamięci, a nie prawdziwy stos.

A jak tego Oracle masz odpalanego na CI? Bo to jest generalnie problem (nawet nie wiem czy nie powoduje dodatkowych kosztów -licencja )

Nie używam oracla, ja tu bardziej mówię o dynamo db, kinesisie, s3 i reszcie stosu w chmurce Amazonu. Część jest utworzona na stałe i czyszczona + wypełniana danymi, część jest tworzona od zera przez infrastructure as a code. Koszty oczywiście są, licencjami się nie przejmuję, bo tym zajmuje się chmura.

1

Akurat nazywanie h2 atrapką to lekka potwarz - to całkiem sensowna baza, która ma pecha, że nie nazywa się oracle, ale że symuluje całkiem nieźle to przyzwyczaiłem się do niej.

U mnie u praktycznie wszytkich klientów kulą u nogi jest oracle (żeby to jeszcze tylko baza danych była...) - jak mamy chmurę to jest to jest to jakaś własna (oparta o kubernetes) - ale do infrastructure as code daleko (raczej przez biurokracje niż brak chęci i umiejętności u zespołów za to odpowiadających).

Zmienia się trocję od kiedy google ma instacje w Zurichu - chociaż jakiś czas temu się musiałem tłumaczyć czemu jakieś połączenia leciały spoza Szwajcarii. Tutejsi mają hopla na punkcie trzymania danych w granicach kraju (serio - lapka z banku (na którym i tak nic ciekawego nie ma) nie mogę wywieźć za granicę co czasem bywało problemem).

1
jarekr000000 napisał(a):

Akurat nazywanie h2 atrapką to lekka potwarz - to całkiem sensowna baza, która ma pecha, że nie nazywa się oracle, ale że symuluje całkiem nieźle to przyzwyczaiłem się do niej.

Nie ma znaczenia, jak symuluje, bo to ciągle nie jest ten produkcyjny komponent, którego używasz.

U mnie u praktycznie wszytkich klientów kulą u nogi jest oracle (żeby to jeszcze tylko baza danych była...) - jak mamy chmurę to jest to jest to jakaś własna (oparta o kubernetes) - ale do infrastructure as code daleko (raczej przez biurokracje niż brak chęci i umiejętności u zespołów za to odpowiadających).

No to jak Wy testujecie? U mnie stawia się pełen stos przedprodukcyjny, robi na nim testy wydajnościowe, integracyjne, porównawcze i całą resztę. Robienie tego na atrapach (niezależnie od ich zgodności z oryginałem) mija się z celem. Tylko nie wyobrażam sobie stawiania tego na swojej maszynie, bo ani nie mam na to ochoty (po co mam trzymać to wszystko u siebie), a znowu wymienianie na mniej lub bardziej inteligentne mocki mnie nie przekonuje.

1
Afish napisał(a):

Nie ma znaczenia, jak symuluje, bo to ciągle nie jest ten produkcyjny komponent, którego używasz.

Moim zdaniem czepiasz się absurdalnie bezsesownego miejsca. Skoro łapie błędy programistów to działa.

No to jak Wy testujecie? U mnie stawia się pełen stos przedprodukcyjny, robi na nim testy wydajnościowe, integracyjne, porównawcze i całą resztę. Robienie tego na atrapach (niezależnie od ich zgodności z oryginałem) mija się z celem. Tylko nie wyobrażam sobie stawiania tego na swojej maszynie, bo ani nie mam na to ochoty (po co mam trzymać to wszystko u siebie), a znowu wymienianie na mniej lub bardziej inteligentne mocki mnie nie przekonuje.

Ja piszę o testowaniu kodu - głównie z punktu widzenia pojedynczej aplikacji. Z punktu widzenia CI.

Testowanie całego stosu to jest kolejny kawałek układanki - większość moich klientów ma tu duże problemy - czyli jest środowisko testowe - zrobione faktycznie jak produkcja - nawet zwykle jest tych środowisk kilka. Ale IMO nie ma na tym automatycznych sensownych testów (są jakieś scenariusze testowe odpalane w straszliwych enterprisy narzędziach - nawet tego nie dotykam). Żeby to nie było bryndzą to trzeba by np. zrobić coś z cobolem - o nieszczęsnym oracle ( z golden gate i oracle forms) nie wspominając..

2
jarekr000000 napisał(a):

Moim zdaniem czepiasz się absurdalnie bezsesownego miejsca. Skoro łapie błędy programistów to działa.

Z taką logiką można powiedzieć, że expectWasCalled też łapie błędy programistów, więc działa. Ja się nie czepiam, ja nie wierzę opowieściom, że jakaś tam emulacja produkcyjnego komponentu zawsze zadziała. Jeżeli to nie jest ten sam kod, to nie jest to produkcja, tyle.

Ty się tylko tym różnisz, że zamiast robić najgłupszego mocka, używasz czegoś bardziej wyrafinowanego, ale ciągle nie jest to ten produkcyjny komponent, który trzeba sprawdzić. Oczywiście to nie musi być problem, jeżeli robię SELECT * FROM tabelka, to każda baza mi to ogarnie, ale jeżeli używam temporal tables albo izolacji transakcji, to się to wysypie.

Ja piszę o testowaniu kodu - głównie z punktu widzenia pojedynczej aplikacji. Z punktu widzenia CI.

No a baza wchodzi w skład CI, czy nie? Bo jak wchodzi, to trzeba ją przetestować bez atrap, a jak nie wchodzi, to dyskusja między H2 a mockową implementacją IEntityProvider rozbija się o coś zupełnie innego, bo żadne z nich nie przetestują produkcyjnego kodu tego komponentu. Jasne, że z H2 przetestujesz "prawdziwe DAL", a z mockiem tego kawałka nie przetestujesz, ale "prawdziwe DAL" i tak nie sprawdza, czy działa z prawdziwym serwerem. A skoro DAL zwraca mi jakieś obiekty, to też nie widzę specjalnej przewagi między zwracaniem obiektów z mocka, a zwracaniem takich samych obiektów z bazy, jeżeli ta baza i tak jest inna, niż na produkcji.

Oczywiście nie twierdzę, że używanie H2 nie ma żadnego sensu. Jak najbardziej dobrze jest mieć takie testy, żebym mógł je odpalić z palca w sekundę, ale przed wyjściem na produkcję i tak chcę to przepuścić przez prawdziwe CI z rzeczywistą infrastrukturą, którą build agent będzie sobie tłukł przez 20 minut. Jeżeli tego nie ma i twierdzimy, że aplikacja działa, bo działała z H2, to jak dla mnie jest to słabe.

No i jeszcze jedna kwestia: mówisz, że H2 dobrze imituje oracla. A co by było, jakbyś zamiast oracla miał bazę, której nie mógłbyś emulować czymś in memory. Co wtedy? Mocki? Stawianie bazy w kontenerze i testy trwające minuty/godziny? Co z jakąś kolejką enterprajs, też ją emulujesz czymś innym i liczysz na to, że deadlettering będzie działał tak samo?

1

Ale z drugiej strony - skoro piszesz testy implementacji, by ją "rozpoznać ogniem" i zrozumieć, co się dzieje pod spodem, to dlaczego masz je usuwać, gdy już zrozumiesz co się dzieje?

To jest jakiś paradoks. Skoro piszesz testy implementacji to musisz wiedzieć co testujesz, a więc musisz poznać szczegóły implementacji, a więc zrozumieć co się dzieje pod spodem, a więc osiągnąłeś cel testu implementacji zanim napisałeś tenże test. Gdzie tu logika?

4
Afish napisał(a):

No i jeszcze jedna kwestia: mówisz, że H2 dobrze imituje oracla. A co by było, jakbyś zamiast oracla miał bazę, której nie mógłbyś emulować czymś in memory. Co wtedy? Mocki? Stawianie bazy w kontenerze i testy trwające minuty/godziny? Co z jakąś kolejką enterprajs, też ją emulujesz czymś innym i liczysz na to, że deadlettering będzie działał tak samo?

I tu ważny punkt do całej dyskusji - ekonomia.

Jakoś tak się składa, że w odróżnieniu od większości programistów piszących na 4programmers nie pracuje w PayPalu, Twittterze ani czymś podobnym.
Pracuję przy kartach płatniczych.
Mogłoby się zdawać, że jak jakiś Hans o 2giej w nocy na resztkach diesla zajedzie na stację benzynową gdzieś w Alpach i nie będzie mógł zatankować, bo jego karta uf Widerluege nie działa to jest poważna sprawa. I w istocie byłoby to dość smutne.

Gdy jednak spojrzeć na to czym się zajmuje - to tylko jedna z 20 aplikacji przy których grzebie ( u tego klienta) to ta krytyczna, która pozwoli (albo nie) Hansowi zatankować.
Ta aplikacja jest testowana dodatkowo na pracowicie poskładanym środowisku (jest tych środowisk kilka) gdzie są komponenty hardwarowe, absurdalne programy w c itp. i nawet Oracle.
Komponenty jak na produkcji - tak jak postulujesz!
Biznes ma specjalny soft do testów (nazwijmy Simulator v0.567 :-) ), gdzie definiuje sobie load testy, a również zwykłe przypadki brzegowe itp.

Z tym, że ja mam tendencje do zapominania o tej aplikacji, głównie dlatego, że nie ja ją oryginalnie napisałem :-)
Teraz zajmuje się rozwojem, a że jest to aplikacja krytyczna to ten rozwój to jakieś 5 dni w roku - 2% mojej pracy.
W krytycznym miejscu dla działania firmy nie chcemy dużo i często grzebać, ani specjalnie kombinować - to ma być proste (nawet jak trzeba dużo bardziej nakombinować inne komponenty).
Żeby było śmieszniej jak coś w w testach od biznesu wyjdzie to ja i tak pokrywam ten problem w mockach - i to czasem w najgorszego rodzaju mockach. Nie ja tą aplikację pisałem, mocki są w niej takie sobie, ale że zmiany robię małę - to puty co nie zmieniam tych mocków na żadne lepsze (nie widzę opłacalności).

Cała reszta aplikacji w tej fimie ma całkiem spoko SLA, jest środowisko przedprodukcyjne testowane głównie ręcznie (choć tam też działa Simulator).
Jeśli nie zadziała wydruk sald - to ludzie nie dostaną listów 2go tylko 7go w danym miesiącu - tragedii nie ma.
Punkty żegnaj się nie naliczą dziś - naliczą się jutro. Coś dwa razy zaksięgujemy - no to wyjdzie problem w jakimś bilansie w ciągu 48 godzin - zrobimy korektę.
Ogólnie luksusy.

Mocki to rozwiązanie tanie, które niewiele kosztuje, ale i też niewiele daje, tym niemniej czasem lepsze niż nic (czasem gorsze niż nic - faktycznie, trzeba uważać).
Testowanie z bazą danych, nie taka jak trzeba, na nie takim os, itd. daje mi znaczny wzrost jakości testów przy widocznym, ale nie tragicznym, wzroście kosztów.
Testowanie CI z prawdziwym oraclem ( u tego klienta) wiązałoby się z istotnie większym kosztem, przy niewielkim wzroście jakości testów. Olewam ten punkt - raczej na zawsze (zwłaszcza że moją misją na oracle jest DROP DATABASE).

Tu jeszcze dodam może, że ogólnie testy to dość absurdalna metoda weryfikacji (słaba). Czasem jak pisze test to mam taki obrazek "matematyków" przed oczami: twierdzenie działa dla 1, działa dla 2 i działa dla 100 - czyli zachodzi dla każdego n.
Jednak testy są po prostu ekonomiczne. Jak widać ekonomia widoczna jest również w samych testach - trzeba rozważyć koszty i dopasować do aplikacji/ SLA / potencjalnych szkód.
Przeważnie jest tak, że dośc tanio da się całkiem dużo przetestować, ale w miarę zwiększania jakości testów ich koszt zaczyna nagle rosnąć nieliniowo.

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