Testy integracyjne vs jednostkowe

0

Po godzinach zacząłem pisać sobie prosty projekt w TDD, naszła mnie jednak pewna refleksja przy pisaniu testów do update'u jakiegoś obiektu.
Do sprawdzenia jest kilka przypadków, wszystko walidowane przez wydzieloną do tego klasę wołaną z głównego serwisu
-czy wszystkie pola nie są nullem / pustym stringiem
-czy jedna z wartości liczbowych jest większa od 0
-czy jeden z ciagów znaków nie przekracza 1024 znaków
-czy podany id w ogóle istnieje

W prawdziwym przypadku wyszło ponad 10 testów jednostkowych, klasa testowa była kilka razy większa od właściwej implementacji. Dodatkowo doszło pisanie interfejsu dla repository i jego implementacji InMemory. Po czasie jednak się zorientowałem, że ta metoda będzie i tak wołana tylko przez http - dorzuciłem więc kilka adnotacji javax.validation.constraints do wejściowego dtosa i w efekcie mogłem wywalić całą klasę odpowiednią za walidację i 3/4 testów jednostkowych. Implementacja na tę chwilę wygląda mniej więcej tak

@Builder
@Getter
public class UpdateItemRequest {
    @NotBlank
    private final String itemId;

    @NotBlank
    @Size(max = 1024)
    private final String description;

    @NotBlank
    @Size(min = 0)
    private final Integer quantity;
}


@RestController
class ItemController {
    @PutMapping
    ResponseEntity<Item> updateItem(@RequestBody @Valid UpdateItemRequest request) {
        return ResponseEntity.ok(itemService.updateItem(request));
    }
}

@Service
public class ItemService {
    Item updateItem(UpdateItemRequest request) {
        return itemRepository.findById(request.getItemId())
                .map(item -> update(item, request))
                .orElseThrow(() -> new RuntimeException("Item not found"));
    }
}

No i teraz pytanie - czy jest sens takie operacje testować jednostkowo?
Czy taki test na bazie InMemory w ogóle ma jakąś wartość?
Czy sam test integracyjny nie byłby tutaj całkowicie wystarczający?
Czy w ogóle podejście TDD ma tutaj sens - sama idea mówi by te testy odpalać praktycznie co chwila, jeżeli miałbym co chwile czekać aż uruchomi się Spring to z miejsca bym zrezygnowal z ich pisania.
No i jeżeli zdecyduje się na pozostanie tylko przy testach integracyjnych - czy warto w nich sprawdzać case typu ujemne quantity, description na więcej niż 1024 znaki czy np. czy @ControllerAdvice złapie moj wyjątek? Czy olać sprawdzanie frameworka i napisać test na happy path?

0

No i teraz pytanie - czy jest sens takie operacje testować jednostkowo?

Testy integracyjne zawsze są lepsze. O ile oczywiście są wystarczająco szybkie

Czy taki test na bazie InMemory w ogóle ma jakąś wartość?

Większy niż mockowanie repository

Czy sam test integracyjny nie byłby tutaj całkowicie wystarczający?

j.w. Jeśli szybkość jest wystarczająca.

Czy w ogóle podejście TDD ma tutaj sens - sama idea mówi by te testy odpalać praktycznie co chwila, jeżeli miałbym co chwile czekać aż uruchomi się Spring to z miejsca bym zrezygnowal z ich pisania.

  1. Przede wszystkim TDD zakłada że testy są pisane przed implementacją
  2. Jeśli Spring jest za wolny to może pora zrezygnować ze Springa? Może lepszy do wystawiania RESTa byłby RatPack ,SparkJava, AkkaHttp lub DropWizard?

No i jeżeli zdecyduje się na pozostanie tylko przy testach integracyjnych - czy warto w nich sprawdzać case typu ujemne quantity, description na więcej niż 1024 znaki czy np. czy @ControllerAdvice złapie moj wyjątek? Czy olać sprawdzanie frameworka i napisać test na happy path?

Spytaj znajomego testera co myśli o otestowywaniu tylko happy patha

5

Właśnie odkryłeś czemu TDD/waterfall nie działa, gratuluje, niektórym zajmuje to dużo więcej czasu :) Zrobiłeś kupę porządnej, nikomu nie potrzebnej roboty, bo naklepałeś milion testów a potem odkryłeś, ze w sumie ten początkowo wymyślony design jest zły (w takim waterfallowym podejściu to chleb powszedni) i trzeba go zmienić, orając przy tym wszystkie te skrzętnie pisane testy. Co więcej, mimo że naklepałeś tyle testów, wcale nie chronią cię przed regresją kiedy teraz przepisałeś tą logikę na nową! Pytanie więc jaką w ogóle miały wartość? Bo co jeśli teraz przypadkiem zamiast 1024 znaki wpisałeś 124 w adontacji, albo zamiast min = 0 dałeś min = 10. Gdybyś miał testy integracyjne, to wyszłoby od razu, bo testy polegałyby tylko na publicznym API twojego serwisu, niezaleznie od wewnętrznej implementacji. A teraz implementacja się zmieniła i wszystkie twoje whiteboxowe testy jednostkowe lecą do kosza.

Czy taki test na bazie InMemory w ogóle ma jakąś wartość?

Jesli normalnie masz tam jakąś SQLową bazę a tutaj wrzuciłes jakieś h2/hsql to moim zdaniem wszystko jest ok. Można zawsze rozwazyć też TestContainers i postawić taką bazę jak na produkcji.

jeżeli miałbym co chwile czekać aż uruchomi się Spring to z miejsca bym zrezygnowal z ich pisania.

A próbowałeś? Bo co chwila ktoś podnosi taki argument że łoooo panie ileż to takie testy będą startować!!11oneone a w praktyce nigdy nie próbowali. Klasyczne urban legends. Pamiętaj że do takich IT testów aplikacje wstaje raz a potem leca testy, więc overhead odpalenia aplikacji sie mocno niweluje. To jest kwestia kilku sekund.

No i jeżeli zdecyduje się na pozostanie tylko przy testach integracyjnych - czy warto w nich sprawdzać case typu ujemne quantity, description na więcej niż 1024 znaki czy np. czy @ControllerAdvice złapie moj wyjątek? Czy olać sprawdzanie frameworka i napisać test na happy path?

Oczywiście że warto! Regresja! Ktoś przypadkiem usunie ci adnotacje albo coś w niej zmieni i będzie problem. Przecież na tym polegają testy, że chcesz sprawdzić czy aplikacja w konkretnej sytuacji zachowuje się w przewidywany sposób.

1

Również całkiem pozytywnie zaskoczyłem się tym, że spring nie wstaje aż tak długo jak by sie wydawało. Nadal jest to jakieś 3-4 sekundy dłużej niż przy jego braku, ale jesli doda się do tego czas budowania projektu, to ten dodatek aż tak dużo nie zmienia. (Inna sprawa, że trzeba sie trochę pobawić wtedy z dokumentacją springa jak te testy odpalać, ale to moze tylko moje doświadczenia)

Zależy też trochę jak masz podział na moduły itp. Obecnie u mnie mamy osobno moduł na kotakt z bazą i on sobie ma testy integracyjnie normalnie napisane, a część z domeną ma swoje testy. Ale to też ma problem taki, że zależy jak bardzo bawisz się w zabiezpieczenia apki, bo jeśli mocno, to wypada też przetestować całe przychodzenie requestów

No i pamiętaj to, że nie testujesz kodu (a wiec nie testujesz frameworka) a testujesz czy Twoja aplikacja działa.

1
Shalom napisał(a):

Właśnie odkryłeś czemu TDD/waterfall nie działa, gratuluje, niektórym zajmuje to dużo więcej czasu :) Zrobiłeś kupę porządnej, nikomu nie potrzebnej roboty, bo naklepałeś milion testów a potem odkryłeś, ze w sumie ten początkowo wymyślony design jest zły (w takim waterfallowym podejściu to chleb powszedni) i trzeba go zmienić, orając przy tym wszystkie te skrzętnie pisane testy.

Ale w TDD iteracja w teorii trwa 30 sekund, a w praktyce kilka minut. Więc nie zwalajmy winy na TDD, bo TDD mówi tylko kiedy powinny być pisane testy a nie jak powinny wyglądać.
Co do waterfallu to jakby klient wiedział co chce i warunki się nie zmieniały to waterfall by działał jak złoto. Przynajmniej co do zbierania i planowania tasków. Co do architektury to wiadomo że nawet dobrze napisany system możliwe że trzeba czasem przepisać bo zmienia się obciążenie (chociaż to znów warunki/środowisko zewnętrzne)

0

Napisałem kilka kolejnych funkcjonalności i jak do tej pory... mniej niż 10 testów jednostkowych, a ponad 60 integracyjnych.
Jednostkowo potraktowałem typowe filtrowanie danych, które odbywało sie po pobraniu czegoś z bazy danych. Nawet przekonałem się do Mocka na repozytorium zamiast implementacji InMemory.
Za to cała reszta kodu jest przetestowana integracyjnie, z prawdziwą bazą danych. Nie narzekam na szybkość odpalania, kilka sekund dłużej na postawienie apki, ale same testy bardzo sprawnie się kończą.

Plusy?
-Po przygotowaniu infrastruktury (fabryk do requestow, jakiegoś szkieletu aplikacji), dopisanie otestowanie nowej funkcjonalności to kwestia kilku minut
-Mam pewność, że jak coś zadziała na tych testach to zadziała na "produkcji", testy jednostkowe jednak nie dają takiej pewności jeżeli bazuje się na H2/Mockach/InMemory

Minusy?
-Często łapalem się na pisaniu testów jednostkowych po implementacji, że trzeba mockować dużo rzeczy, że ciężko w ogóle przygotować taki test. Pisząc testy przed kodem zgodnie z TDD, taki problem się nie pojawiał. Jeżeli jednak wejściem testu jest strzał http, a asercję wykonuje przed oczyt repozytorium, to nie mam jak sprawdzić czy wewnętrzna implementacja jest ok, czy nie siedzi tam spaghetti. Jednym zdaniem, sprawdzam zachowanie aplikacji, ale w żaden sposób nie sprawdzam jakości implementacji.

No i sam sobie zadaję pytanie - czy podejście które stosuje faktycznie sprawdzi na dłuższą metę? Czy faktycznie nie ma sensu pisać testów jednostkowych jeżeli nie ma skomplikowanej logiki do przetestowania?

3

to nie mam jak sprawdzić czy wewnętrzna implementacja jest ok

I chwała ci za to! Testy służą do sprawdzenia czy aplikacja działa / czy spełnia wymagania funkcjonalne, a nie czy kod wygląda tak samo jak kiedy pisałem testy. Widziałem już sporo takich projektów które ktoś zabetonował testami. Cokolwiek nie dotkniesz to milion testów failuje...

nie sprawdzam jakości implementacji

Testy nigdy jej nie sprawdzają. To trochę mitologia że unit testy uratują cię przed spaghetti, a juz na pewno nie w projekcie który się ciagnie dłuzszy czas. Bo zapominasz chyba o tym, ze kod ewoluuje, tu sie coś do niego dopisze, tam się dopisze. Zresztą widziałem juz testy "jednostkowe" gdzie setup mocków podchodził pod 200 linii
Jeśli chcesz pilnować jakości to code review do tego służy.

Często łapalem się na pisaniu testów jednostkowych po implementacji, że trzeba mockować dużo rzeczy, że ciężko w ogóle przygotować taki test

Nie widzę związku. Testy jednostkowe źle sie pisze do kodu który jest słabo napisany, z tym się zgodzę, ale jak ktoś nie umie napisać tego kodu czytelnie i prosto, to napisanie testów wcześniej tego nie zmieni. I znów -> code review.

0
Jednostkowo potraktowałem typowe filtrowanie danych, które odbywało sie po pobraniu czegoś z bazy danych.

A tak z ciekawości nie mogłeś filtrować od razu na poziomie sql czy czego tam używasz?

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