Testy możliwie integracyjne: czy to nie czyni wielu dobrych praktyk bezsensownymi?

1

Czytam sobie wątki:

I artykuły:

Jak rozumiem, wielu sądzi, że testowanie poszczególnych klas czy metod bardzo często kompletnie nie ma sensu. Zamiast tego należy pisać testy możliwie jak najbardziej integracyjne, tak bardzo integracyjne, jak to tylko praktycznie możliwe.

A zatem: API Restowe najchętniej testowac należy w ten sposób, że się wysyła zapytania i sprawdza odpowiedzi. Webapkę natomiast za pomocą jakiegoś Selenium albo coś. To przynajmniej jest ideał, od którego trzeba będzie odstępować niekiedy schodząc czasem nawet do poziomu testowania poszczególnych metod, ale zaczynamy od e2e i schodzimy niżej jeśli musimy, a nie na odwrót.

Ma to dla mnie sens.

Przemyślenia mnie jednak naszły, jak to się ma do wielu "dobrych praktyk" powszczechnie gloszonych?

Oddzielanie logiki od interfejsu

Wielu promuje, by niezwykle rygorystycznie oddzielać logikę od interfejsu. Oznacza to na przykład, że nie ma się prawa wstrzyknąć bazy danych do kontrolera w RESTowym API: zadania kontrolera ograniczać się mają do obsługi spraw związanych z HTTP oraz przekazania odpowiedzialności za chociażby pobranie z bazy danych wierszy, które trzeba zwrócić w odpowiedzi do jakiś serwisów albo innych komend CQRS.

Celem takiego postępowania, jak rozumiem, jest:

  • Łatwość pisania unit testów,
  • Łatwość tworzenia wielu interfejsów do jednej aplikacji.

Jednak jeśli testujemy, o ile to możliwe, integracyjnie, to kwestia unit testów nam spada całkowicie. Co do wielu interfejsów, to najczęściej YAGNI - w 99% przypadków interfejs będzie zawsze i tylko jeden.

Inversion of Control oraz Dependency Injection

Celem wstrzykiwania zależności do klas (zamiast walenia new albo static) jest, jak rozumiem:

  • Łatwość pisania unit testów,
  • Łatwość wyboru implementacji jakiejś zależności w runtime.

Przy czym są dwa możliwe użycia wstrzykiwania zależności. Można wstrzykiwać samą implemetację, albo fabrykę. W tym pierwszym przypadku nie uzyskujemy nic, jeśli chodzi o wybór implementacji w runtime, to nam służy tylko do unit testów.

A zatem

Czy dobrze rozumuję, że jeśli odchodzimy od unit testów do testów możliwie integracyjnych, to oddzielanie logiki od interfejsu oraz wstrzykiwanie zależności powoli przestaje mieć sens?

Tam, gdzie istotnie będziemy mieć wiele UI, tam musimy dalej pilnować rygorystycznego podziału. Jeśli mamy tylko jeden UI, ale w jakiejś części kodu dla jakiejkolwiek przyczyny musimy zejść na poziom unit testów, tam dalej musimy oddzielić tę logikę od interfejsu. Ale w pozostałych przypadkach możemy chyba mieć logikę w kontrolerze?

Co do wstrzykiwania zależności: Znowu to samo, tam gdzie musimy zejść na poziom unit testów, tam musimy wstrzyknąć w jakąś klasę zależności, które muszą być zfakowane podczas testów. Ale w pozostałych przypadkach wstrzykiwanie zależności traci swój sens i cel, a tylko dodaje cognitive overhead?

Ciekawe jest podejście z artykułu https://tyrrrz.me/blog/unit-testing-is-overrated, który zalinkowałem wyżej, a który - z mojego doświadczenia - bywa niekiedy rzucany w internetach, by poprzeć opinię, że należy testować przede wszystkim integracyjnie.

Autor tam najpierw prezentuje kod, wg niego "poprawny z punktu widzenia OOP", ale który jest trudny do unit testowania: (ten kod nie używa DI!)

public class LocationProvider : IDisposable
{
    private readonly HttpClient _httpClient = new HttpClient();

    // Gets location by query
    public async Task<Location> GetLocationAsync(string locationQuery) { /* ... */ }

    // Gets current location by IP
    public async Task<Location> GetLocationAsync() { /* ... */ }

    public void Dispose() => _httpClient.Dispose();
}

public class SolarCalculator : IDisposable
{
    private readonly LocationProvider _locationProvider = new LocationProvider();

    // Gets solar times for current location and specified date
    public async Task<SolarTimes> GetSolarTimesAsync(DateTimeOffset date) { /* ... */ }

    public void Dispose() => _locationProvider.Dispose();
}

Następnie autor refaktoruje ten kod, by można było go unit testować (już mamy DI!)

public interface ILocationProvider
{
    Task<Location> GetLocationAsync(string locationQuery);

    Task<Location> GetLocationAsync();
}

public class LocationProvider : ILocationProvider
{
    private readonly HttpClient _httpClient;

    public LocationProvider(HttpClient httpClient) =>
        _httpClient = httpClient;

    public async Task<Location> GetLocationAsync(string locationQuery) { /* ... */ }

    public async Task<Location> GetLocationAsync() { /* ... */ }
}

public interface ISolarCalculator
{
    Task<SolarTimes> GetSolarTimesAsync(DateTimeOffset date);
}

public class SolarCalculator : ISolarCalculator
{
    private readonly ILocationProvider _locationProvider;

    public SolarCalculator(ILocationProvider locationProvider) =>
        _locationProvider = locationProvider;

    public async Task<SolarTimes> GetSolarTimesAsync(DateTimeOffset date) { /* ... */ }
}

Autor narzeka, że ten "poprawiony" kod jest dużo bardziej skomplikowany niż oryginalny i że to daje sporo cognitive overhead, a wszystko tylko po to, by mogły być unit testy.

Zatem autor dalej pokazuje, jak wyrzucić większość unit testów i testować integracyjnie. W finalnej wersji kodu autor pozwala sobie na herezję, czyli logikę w kontrolerze, jednak dalej stosuje DI:

[ApiController]
[Route("solartimes")]
public class SolarTimeController : ControllerBase
{
    private readonly SolarCalculator _solarCalculator;
    private readonly LocationProvider _locationProvider;
    private readonly CachingLayer _cachingLayer;

    public SolarTimeController(
        SolarCalculator solarCalculator,
        LocationProvider locationProvider,
        CachingLayer cachingLayer)
    {
        _solarCalculator = solarCalculator;
        _locationProvider = locationProvider;
        _cachingLayer = cachingLayer;
    }

    [HttpGet("by_ip")]
    public async Task<IActionResult> GetByIp(DateTimeOffset? date)
    {
        var ip = HttpContext.Connection.RemoteIpAddress;
        var cacheKey = ip.ToString();

        var cachedSolarTimes = await _cachingLayer.TryGetAsync<SolarTimes>(cacheKey);
        if (cachedSolarTimes != null)
            return Ok(cachedSolarTimes);

        // Composition instead of dependency injection
        var location = await _locationProvider.GetLocationAsync(ip);
        var solarTimes = _solarCalculator.GetSolarTimes(location, date ?? DateTimeOffset.Now);

        await _cachingLayer.SetAsync(cacheKey, solarTimes);

        return Ok(solarTimes);
    }

    /* ... */
}
6

Zależy którego rabina spytasz. Nawet obóz TDD-owiczów dzieli się na London i Detroit.

Poza tym wrzucenie wszystkiego do kontrolera, bo tak się łatwiej testuje to tak jak pisanie długich metod, bo się łatwiej debugguje. No niby tak, ale nie do końca. Moim zdaniem argumentacja i ten kod to śmieć, nie każdy artykuł napisany na medium jest wyznacznikiem jakości.

Drugi poziom niezrozumienia to poziom abstrakcji. Niektóre rzeczy wygodnie testuje się w odseparowaniu od zależności, bo chcemy przetestować na wylot konkretny fragment, a czasami chcemy wiedzieć czy cała funkcjonalność działa. Nie trzeba do tego skomplikowanej nomenklatury, tylko zdrowego rozsądku- wystarczy sobie zadać zajebiście ważne pytanie jakich potrzebuję, żeby wiedzieć, że aplikacja będzie działać tak jak chcę. I w zależności od aplikacji te testy i ich ilość będą się różnić.

0

London to już chyba kompletna aberracja, kwintesencja testowania implementacji, a nie funkcjonalności... Betonowanie kodu, bo to właśnie implementancja jest usztywniona testami...

Poza tym wrzucenie wszystkiego do kontrolera, bo tak się łatwiej testuje

? chyba nie dlatego, że tak się łatwiej testuje, tylko dlatego, że to już nie ma większego znaczenia?

Drugi poziom niezrozumienia to poziom abstrakcji. Niektóre rzeczy wygodnie testuje się w odseparowaniu od zależności

Tak, to ma znazcenie glównie wtedy, jak ten fragment zawiera sporo logiki, zaś mało zależności od infrastruktury. O tym nawet ten autor pisał.

Moim zdaniem argumentacja i ten kod to śmieć

Uczciwie trzeba przyznać, że ja nie postawiłem sobie za zadanie podsumowania / streszczenia artykułu. Argumentacja jest tam trochę inna - zgrubsza, że test powinien testować zachowanie aplikacji a nie to, jak ona jest napisana. Ja tylko wyciągnąłem to, co mi było potrzebne do tego postu.

EDIT: Zresztą, o ile pamiętam, na tym forum Shalom też był zwolennikiem BDD

0

Autor tam najpierw prezentuje kod, wg niego "poprawny z punktu widzenia OOP", ale który jest trudny do unit testowania: (ten kod nie używa DI!)

Nie wiem czy to jest poprawny kod z punktu widzenia OOP, ale soliD to on nie jest.

4

Jak rozumiem, wielu sądzi, że testowanie poszczególnych klas czy metod bardzo często kompletnie nie ma sensu. Zamiast tego należy pisać testy możliwie jak najbardziej integracyjne, tak bardzo integracyjne, jak to tylko praktycznie możliwe.

Z pierwszą częścią się całkowicie zgadzam. Nie testujemy klas i metod tylko funkcjonalność.

Z drugą zupełnie nie.
Testujemy funkcjinalności. Nie integracyjnie, nie unitowo tylko tak, żeby została przetestowana.
Jak wystawiam api po http, to naturalnym sposobem przetestowania tego jest zrobienie wywołania http.

Jak robię serwis, który ma być użyty w endpointach HTTP, ale teraz nie robię endpointów tylko ten serwis - to testuję go robiąc bezpośrednie wywołania metod.

Co do bezsensowności DI, kiedy stosujesz testowanie normalne - bez mocków. To masz bardzo dobrą intuicję.
W zasadzie nie jest potrzebne - teoretycznie.
Tylko prawda jest taka, że prawie zawsze coś jednak idzie inaczej niż na produkcji,
a to baza inna, albo ma inny url, albo inny init (no bo jak projekt startuje z main to podłącza się do jakiejś wystartowanej bazy, a jak z testów to do jakiejś chwilowo wystartowanej instancji, która jeszcze jest po każdym teście czyszczona)
Czas jest inny zawsze (o ile dobrze umiemy "zarządzać czasem" w testach).
Więc zawsze się (niestety) trochę mockuje/oszukuje. Chociaż niekoniecznie przez stosowanie wprost mocków. Dobrym przykładem jest Clock - gdzie w testach używam takiego, w którym mogę explicite ustawić czas. A na produkcji idzie z zegara systemowego w 99% testów.
Aczkolwiek dla pewności zrobię też ten problematyczny test z zegarem systemowym.
I na takie rzeczy właśnie jest DI, żeby w razie potrzeby można było coś przepiąć. A, że nie wiadomo co się przyda i gdzie ... no to się często udostępnia bardzo dużo jako zależności.

0
YetAnohterone napisał(a):

Co do wstrzykiwania zależności: Znowu to samo, tam gdzie musimy zejść na poziom unit testów, tam musimy wstrzyknąć w jakąś klasę zależności, które muszą być zfakowane podczas testów. Ale w pozostałych przypadkach wstrzykiwanie zależności traci swój sens i cel, a tylko dodaje cognitive overhead?

Trochę tak ale i nie. To jakie zalety widzę:

  • składając aplikację przez DI widać fajny logiczny podział kodu. To trochę jak darmowa dokumentacja
  • jeśli w kodzie mam jedną instancje do obsługi HTTP albo czasu to po samym drzewku zależności, który składam w okolicy maina widać, kto w kodzie rozmawia po HTTP albo kto jest zbryzgany nieczystością w postaci zmiennego czasu
  • jak masz interfejs to łatwo dodać niefunkcjonalne/poza-funkcjonalne aspekty do kodu (to do czego miał służyć AOP). Np. mogę owinąć repozytorium jakimś in-memory cachem, który implementuje ten sam interfejs (wzorzec Adapter).
  • użycie interfejsów pozwala na lepszą modularność. Jakbyś wszystko miał bez DI to wszystko musiałbyś mieć cały kod w tym samym module/pakiecie
4
YetAnohterone napisał(a):

e test powinien testować zachowanie aplikacji a nie to, jak ona jest napisana.

Tylko nie zawsze jest sens testować działanie całej aplikacji, czasami warto przetestować jakiś jeden algorytm. Do tego służą testy jednostkowe. Testowanie jednej jednostki, to testowanie części kodu, a nie tego jak "aplikacja jest napisana".
Nie zawsze pisze się też całe aplikacje. Np. ja często piszę biblioteki. Zdaniem tego gościa mam nie testować?

1

A nie mozna testowac jednostkowo oraz do tego sprawdzac funkcjonalnosci (w osobnych testach)? Jezeli mamy endpoint do sortowania to moge przeciez:

  1. przetestowac wystawiony endpoint przykladowo robiac http requesta dla rest api.
  2. przetestowac serwis ktory dziala na tym endpoincie
  3. przetestowac samo konkretne sortowanie.

Dlaczego mialbym z czegos rezygnowac?

3
Seken napisał(a):

A nie mozna testowac jednostkowo oraz do tego sprawdzac funkcjonalnosci (w osobnych testach)? Jezeli mamy endpoint do sortowania to moge przeciez:

  1. przetestowac wystawiony endpoint przykladowo robiac http requesta dla rest api.
  2. przetestowac serwis ktory dziala na tym endpoincie
  3. przetestowac samo konkretne sortowanie.

Dlaczego mialbym z czegos rezygnowac?

Wiadomo, że można. Pytanie, czy komuś się chce. Każda warstwa testów wydłuża development, bo przy zmianie funkcjonalności trzeba zmieniać testy w 3 miejscach a nie w 1. Do tego testy na niższych warstwach częściej się zmieniają, przez co mniej im się ufa. Nie ma nic lepszego, niż przeorać całą aplikację bez zmiany ani jednego kodu z testami

3
Seken napisał(a):
  1. przetestowac wystawiony endpoint przykladowo robiac http requesta dla rest api.
  2. przetestowac serwis ktory dziala na tym endpoincie
  3. przetestowac samo konkretne sortowanie.
  1. przetestować napięcia zasilacza i czy nie jest z czarnej listy

Jasne, że można, pytanie "po co?". Jeżeli przetestujesz rzetelnie pkt. 1 to przetestujesz cały kod, który jest wymagany do jego działania. Czy dopisując jakiekolwiek dodatkowe kawałki przetestujesz coś lepiej? Jaka będzie z tego korzyść?

0
YetAnohterone napisał(a):

Jak rozumiem, wielu sądzi, że testowanie poszczególnych klas czy metod bardzo często kompletnie nie ma sensu. Zamiast tego należy pisać testy możliwie jak najbardziej integracyjne, tak bardzo integracyjne, jak to tylko praktycznie możliwe.

Już w tym zdaniu został popełniony błąd, więc czytać więcej nie trzeba. Między testowaniem pojedynczej klasy a całej aplikacji jest 50 poziomów wielkości kodu ( :P )

Można testować po 2 klasy, można - po 20. Ważne żeby przetestować funkcjonalność (co już pewnie ktoś wcześniej napisał), a w zasadzie konkretną funkcję biznesową (bo funkcjonalność to podobno zbiór wszystkich funkcji)

BTW

Webapkę natomiast za pomocą jakiegoś Selenium albo coś

To prawie zawsze prowadzi do bólu i rozpaczy, bo z jakiegoś powodu testy frontu są o wiele bardziej niestabilne niż testy gołego HTTP (REST) API

0

@Seken:

No ale juz odpowiadając konkretnie. 3 testujemy przykładowo jeżeli interesuje nas konkretny rodzaj sortowania, a nie tylko czy posortowaliśmy. W 2 analogicznie możemy testować rzeczy do których nie potrzebujemy zaprzęgać całego systemu i po co mamy to robić.

Ale co ci da przetestowanie tego konkretnego rodzaju sortowania, jeżeli nie masz pojęcia, czy gdziekolwiek zostało użyte i czy efekt jest całości (w tym użyci tego sortowania) jest zgodny z oczekiwaniami?
A jeżeli masz test na wyższym poziomie, który to robi, to po co pisać test szczegółowy?

2
piotrpo napisał(a):

A jeżeli masz test na wyższym poziomie, który to robi, to po co pisać test szczegółowy?

Bo ogólny będzie trwać za długo. Mogę sobie przetestować raz integracyjnie że system sortuje. A potem 10 razy jednostkowo różne dziwne przypadki brzegowe.
Albo 10 razy integracyjnie a potem 100 razy w properties based testing. PBT niestety trwa długo, a jeszcze dłużej trwają testy mutacyjne

0
Seken napisał(a):

A nie mozna testowac jednostkowo oraz do tego sprawdzac funkcjonalnosci (w osobnych testach)? Jezeli mamy endpoint do sortowania to moge przeciez:

  1. przetestowac wystawiony endpoint przykladowo robiac http requesta dla rest api.
  2. przetestowac serwis ktory dziala na tym endpoincie
  3. przetestowac samo konkretne sortowanie.

Dlaczego mialbym z czegos rezygnowac?

A pisałeś sortowanie sam? Bo jeśli nie, to bym z niego zrezygnował.
A jeśli serwis nie robi nic poza zwracaniem danych do API pobranych skądś tam, to też bym zrezygnował.

Co innego, gdyby serwis robił coś mądrego przy okazji. Albo gdybym pisał sortowanie sam.

0
KamilAdam napisał(a):

Bo ogólny będzie trwać za długo. Mogę sobie przetestować raz integracyjnie że system sortuje. A potem 10 razy jednostkowo różne dziwne przypadki brzegowe.
Albo 10 razy integracyjnie a potem 100 razy w properties based testing. PBT niestety trwa długo, a jeszcze dłużej trwają testy mutacyjne

W jakich technologiach/frameworkach piszesz, że jest to problem? Z doświadczenia wiem, że najgorzej w takich testach wypada stawianie testowych kontenerów, ale i to się da jakoś zoptymalizować. W takim pythonie czy go narzut na stawianie aplikacji jest minimalny, do tego testy integracyjne często można puszczać równolegle

0

@KamilAdam: Jasne, tylko to jest jakieś odstępstwo od zasady i być może "jednostka" jest za duża.

0
piotrpo napisał(a):

@Seken:

No ale juz odpowiadając konkretnie. 3 testujemy przykładowo jeżeli interesuje nas konkretny rodzaj sortowania, a nie tylko czy posortowaliśmy. W 2 analogicznie możemy testować rzeczy do których nie potrzebujemy zaprzęgać całego systemu i po co mamy to robić.

Ale co ci da przetestowanie tego konkretnego rodzaju sortowania, jeżeli nie masz pojęcia, czy gdziekolwiek zostało użyte i czy efekt jest całości (w tym użyci tego sortowania) jest zgodny z oczekiwaniami?
A jeżeli masz test na wyższym poziomie, który to robi, to po co pisać test szczegółowy?

Czas, zarówno potrzebny na wykonanie testów jak i debugowanie. Podążanie do ogółu do szczegółu. Nawet jeśli się coś skaszani to łatwiej jest to wyłapać na mniejszych testach niż potem grzebać w logach przez 10 minut i zastanawiać się w którym konkretnie momencie to padło. Szczególnie jest to istotne kiedy obecni developerzy nie są autorami tych rozwiązań.

0
jarekr000000 napisał(a):

Testujemy funkcjinalności. Nie integracyjnie, nie unitowo tylko tak, żeby została przetestowana.
Jak wystawiam api po http, to naturalnym sposobem przetestowania tego jest zrobienie wywołania http.

Jak robię serwis, który ma być użyty w endpointach HTTP, ale teraz nie robię endpointów tylko ten serwis - to testuję go robiąc bezpośrednie wywołania metod.

No dobrze: weźmy uproszczony przykład, sortowanie.

Masz do napisania endpoint, który otrzymuje nieuporządkowaną listę i zwraca posortowaną listę.

W tym celu zaczynasz od napisania serwisu, który sortuje. (For the sake of argument zapomnijmy, że sortowanie to rozwiązany problem i że biblioteka standardowa chyba każdego języka udostępnia funkcje sortujące).

Napisałeś więc funkcję / metodę / whatever, która otrzymawszy nieuporządkowaną listę wypluwa z siebie uporządkowaną listę, nazwijmy tę metodę Sort(). Napisałeś także do niej testy pokrywające wiele przypadków oraz edge case'ów, takich jak dane randomowe, uporządkowane odwrotnie itp.

Teraz dopiero zabierasz się za pisanie endpointa HTTP (nazwijmy go GET /Sort), który będzie wołał funkcję Sort(). Wypadałoby przetestować ten endpoint, bo być może źle został połączony z funkcją Sort() i jest no-opem?

Jak przetestować teraz ten endpoint?

Widzę 4 możliwości, wszystkie złe(?):

  1. Wysyłamy żądanie do endpointa, w endpoincie mockujemy serwis sortujący, sprawdzamy, czy kontroler prawidłowo woła serwis sortujący. Ale tego właśnie chcemy uniknąć.
  2. Wysyłamy żądanie do endpointa, nic nie mockujemy, sprawdzamy, czy endpoint zwraca posortowaną listę. Ale w takim razie w zasadzie copy-pastujemy testy serwisu do testów endpointa.
  3. Jak wyżej, ale usuwamy testy serwisu, by uniknąć duplikacji kodu. To wydaje mi się być najlepszym spośród tych czterech pomysłów, ale jest o tyle złe, że nie możemy teraz z serwisu zrobić biblioteki do re-używania niezależnie od naszego endpointa.
  4. Twierdzimy, że kontroler endpointa ma być pozbawiony logiki i w związku z tym nie musi być pokryty testami (dlatego żądamy, by wartstwa UI, do której należy kontroler, była w opór cienka). Jednak testy endpointa będą chyba najbardziej wartościowe, bo użytkownik widzi wlaśnie endpoint i błędy takie, jak kontroler źle wołający metodę Sort(), kontroler w ogóle nie wołający metody Sort(), kontroler źle zwracający w odpowiedzi dane otrzymane od metody Sort() itp. to są jedne z najpoważniejszych regresji, przed którymi chcemy się bronić.
2
YetAnohterone napisał(a):

Masz do napisania endpoint, który otrzymuje nieuporządkowaną listę i zwraca posortowaną listę.

W tym celu zaczynasz od napisania serwisu, który sortuje.

Ja bym zaczął od napisania endpointu

0
YetAnohterone napisał(a):
  1. Wysyłamy żądanie do endpointa, w endpoincie mockujemy serwis sortujący, sprawdzamy, czy kontroler prawidłowo woła serwis sortujący. Ale tego właśnie chcemy uniknąć.
  2. Wysyłamy żądanie do endpointa, nic nie mockujemy, sprawdzamy, czy endpoint zwraca posortowaną listę. Ale w takim razie w zasadzie copy-pastujemy testy serwisu do testów endpointa.
  3. Jak wyżej, ale usuwamy testy serwisu, by uniknąć duplikacji kodu. To wydaje mi się być najlepszym spośród tych czterech pomysłów, ale jest o tyle złe, że nie możemy teraz z serwisu zrobić biblioteki do re-używania niezależnie od naszego endpointa.
  4. Twierdzimy, że kontroler endpointa ma być pozbawiony logiki i w związku z tym nie musi być pokryty testami (dlatego żądamy, by wartstwa UI, do której należy kontroler, była w opór cienka). Jednak testy endpointa będą chyba najbardziej wartościowe, bo użytkownik widzi wlaśnie endpoint i błędy takie, jak kontroler źle wołający metodę Sort(), kontroler w ogóle nie wołający metody Sort(), kontroler źle zwracający w odpowiedzi dane otrzymane od metody Sort() itp. to są jedne z najpoważniejszych regresji, przed którymi chcemy się bronić.

Ja bym zrobił 2., bo pokazuje, że kontroler jest spięty z serwisem sortującym, a ten jest już przetestowany. Więc skoro testy sortowania już mam, to jeden test na połączenie jednego z drugim mi wystarczy.
4. jest nie na temat, bo mówimy o teście integracyjnym endpointa, a nie jednostkowym kontrolera.

KamilAdam napisał(a):

Ja bym zaczął od napisania endpointu

Dlaczego?
Jak się okaże, że nie umiem sortować, to zostanę z jakimś bezużytecznym endpointem i po co mi on?

1
somekind napisał(a):

Dlaczego?
Jak się okaże, że nie umiem sortować, to zostanę z jakimś bezużytecznym endpointem i po co mi on?

Bo potrzeba sortowania wynika z funkcjonalności biznesowej, a nie odwrotnie.

2
somekind napisał(a):
KamilAdam napisał(a):

Ja bym zaczął od napisania endpointu

Dlaczego?

Bo wtedy łatwiej mi dogadać API jakie trzeba wystawić

Jak się okaże, że nie umiem sortować, to zostanę z jakimś bezużytecznym endpointem i po co mi on?

To skopiujesz z SO. Wszyscy tak robią :P

0
somekind napisał(a):

Ja bym zrobił 2., bo pokazuje, że kontroler jest spięty z serwisem sortującym, a ten jest już przetestowany. Więc skoro testy sortowania już mam, to jeden test na połączenie jednego z drugim mi wystarczy.

Nie jestem pewny, czy dobrze rozumiem.
Piszesz: Ja bym zrobił 2, ale 2 u mnie to jest przeklejenie wszystkich testów serwisu sortującego, ale zaraz potem piszesz jeden test.
Żeby się upewnić: Czy dobrze rozumiem, że chodzi ci o przeklejenie tylko jednego testu z serwisu sortującego do testów endpointa, natomiast pominięcie wszystkich pozostałych testów sprawdzających przypadki brzegowe itp?

KamilAdam napisał(a):

Ja bym zaczął od napisania endpointu

Jeśli problem jest skomplikowany, to czasem wygodniej iść bottom up.

Ale nawet, jeśli idziemy top down: to i tak nie zawsze pomoże. Sam piszesz:

KamilAdam napisał(a):
YetAnohterone napisał(a):

Webapkę natomiast za pomocą jakiegoś Selenium albo coś

To prawie zawsze prowadzi do bólu i rozpaczy, bo z jakiegoś powodu testy frontu są o wiele bardziej niestabilne niż testy gołego HTTP (REST) API

Jeśli tak, to nagle wracamy do punktu wyjścia: jeśli nie wystawiamy endpionta HTTP do sortowania, ale piszemy sortującą webapkę, to nie możemy zgodnie z tym, co napisałeś, zacząć od webapki i pokryć jej testami sprawdzającymi wszystkie przypadki brzegowe sortowania, tylko musimy pokryć tymi testami serwis sortujący. A więc co z testami frontu?

0
piotrpo napisał(a):

Bo potrzeba sortowania wynika z funkcjonalności biznesowej, a nie odwrotnie.

No i co to zmienia? Jeśli nie da się zaimplementować wymagań biznesowych, to niedziałający endpoint w dalszym ciągu ich nie realizuje.

KamilAdam napisał(a):

Bo wtedy łatwiej mi dogadać API jakie trzeba wystawić

Ok, ale to nie jest powód techniczny tylko związany z planowaniem i organizacją pracy. Nie każdy jest tak bardzo agile, żeby się brać za implementację niezdefiniowanych zadań.

YetAnohterone napisał(a):

Nie jestem pewny, czy dobrze rozumiem.
Piszesz: Ja bym zrobił 2, ale 2 u mnie to jest przeklejenie wszystkich testów serwisu sortującego, ale zaraz potem piszesz jeden test.
Żeby się upewnić: Czy dobrze rozumiem, że chodzi ci o przeklejenie tylko jednego testu z serwisu sortującego do testów endpointa, natomiast pominięcie wszystkich pozostałych testów sprawdzających przypadki brzegowe itp?

Faktycznie, umknęło mi, że napisałeś o wszystkich testach. W takim razie moja odpowiedź to 2b. :) Piszę jeden, no może dwa testy dla tego endpointa, na pewno nie powielam wszystkich możliwości z testów jednostkowych.

0
somekind napisał(a):
KamilAdam napisał(a):

Bo wtedy łatwiej mi dogadać API jakie trzeba wystawić

Ok, ale to nie jest powód techniczny tylko związany z planowaniem i organizacją pracy. Nie każdy jest tak bardzo agile, żeby się brać za implementację niezdefiniowanych zadań.

OK, zacznę od endpointu bo ... to jest bardziej w duchu TDD :D

  1. Napisze endpoint który przyjmuje listę
  2. Dodam zwracanie listy w endpoincie
  3. Dodam sortowanie pomiędzy
2

A może tak:
zaczynasz:
assertEquals({"Ala", "Bartłomiej", "Celina"}, httpClient.post(LOCALHOST+"/sort", {"Celina", "Bartłomiej", {Ala})

Stwierdzasz, ze nie masz tego klienta, więc go sobie importujesz, doprowadzasz test do działania, w sensie wysyłania requestu.
Dopisujesz szkielet do uruchomienia serwera, endpoint zwracający puste dane - test nadal czerwony.
Piszę kontroler - test czerwony
dopisuję mapowanie na tę bibliotekę do sortowania (jeszcze nie istniejącą), dopisuję zewnętrzne api biblioteki - test czerwony
piszę return {"Ala", "Bartłomiej", "Celina"} - test zielony
dopisuję drugi test zamiast Celina jest Zbigniew - test czerwony
Piszę to sortowanie
testy zielone, job done

2
YetAnohterone napisał(a):

Oddzielanie logiki od interfejsu

Celem takiego postępowania, jak rozumiem, jest:

  • Łatwość pisania unit testów,

że co

  • Łatwość tworzenia wielu interfejsów do jednej aplikacji.

to też całości powodów nie wyczerpuje

0

Unit testy są reklamowane poprzez fakt, że niejako wymuszają lepszy design - mniejsze klasy, mniej zależności. Po prostu nie wpadniesz na pomysł napisania klasy z 30 parametrami i czytaniem z ftp w losowych metodach statycznych, jeśli będziesz mieć przykaz z góry, że wszystko ma być obtestowane unitowo.

To, że ostatnio unit testy są mniej modne (i słusznie, bo są nadużywane), nie inwaliduje zasad podstawowych.

Jeśli mówimy o IoC i DI, to skupiasz się na dynamicznej podmianie strategii, ale nie zwracasz uwagi, na to, że tniemy zależności aby ograniczyć cognitive load.

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