Jak przetestować system, w którym trudno wyśmiewać obiekty?

34

Pracuję z następującym systemem:

Network Data Feed -> Third Party Nio Library -> My Objects via adapter pattern

Niedawno mieliśmy problem polegający na tym, że zaktualizowałem używaną przeze mnie bibliotekę, co spowodowało między innymi zmianę znaczników czasu (zwracanych przez bibliotekę zewnętrzną long) z milisekund po epoce na nanosekundy po epoce.

Problem:

Jeśli napiszę testy, które kpią z obiektów biblioteki innej firmy, mój test będzie błędny, jeśli popełniłem błąd co do obiektów biblioteki innej firmy. Na przykład nie zdawałem sobie sprawy, że znaczniki czasu zmieniły precyzję, co spowodowało potrzebę zmiany w teście jednostkowym, ponieważ moja próbka zwróciła nieprawidłowe dane. To nie jest błąd w bibliotece , stało się tak, ponieważ coś przeoczyłem w dokumentacji.

Problem polega na tym, że nie mogę mieć pewności co do danych zawartych w tych strukturach danych, ponieważ nie mogę wygenerować prawdziwych bez prawdziwego pliku danych. Obiekty te są duże i skomplikowane i zawierają wiele różnych danych. Dokumentacja biblioteki innej firmy jest słaba.

Pytanie:

Jak skonfigurować testy, aby przetestować to zachowanie? Nie jestem pewien, czy uda mi się rozwiązać ten problem w teście jednostkowym, ponieważ sam test może łatwo się pomylić. Dodatkowo zintegrowany system jest duży i skomplikowany i łatwo coś przeoczyć. Na przykład w powyższej sytuacji poprawnie wyregulowałem obsługę znaczników czasu w kilku miejscach, ale przeoczyłem jedno z nich. W moim teście integracyjnym system wydawał się robić przede wszystkim właściwe rzeczy, ale kiedy wdrożyłem go do produkcji (która ma dużo więcej danych), problem stał się oczywisty.

Obecnie nie mam procesu testów integracyjnych. Testowanie jest w gruncie rzeczy: staraj się utrzymać testy jednostkowe w dobrym stanie, dodaj więcej testów, gdy coś się psuje, a następnie wdróż na moim serwerze testowym i upewnij się, że wszystko wydaje się rozsądne, a następnie wdróż w produkcji. Ten problem ze znacznikiem czasu przeszedł testy jednostkowe, ponieważ symulacje zostały utworzone niepoprawnie, a następnie przeszedł test integracji, ponieważ nie spowodował żadnych natychmiastowych, oczywistych problemów. Nie mam działu kontroli jakości.

durron597
źródło
3
Czy możesz „nagrać” prawdziwy plik danych i „odtworzyć” go później w bibliotece strony trzeciej?
Idan Arye,
2
Ktoś mógłby napisać książkę o takich problemach. W rzeczywistości Michael Feathers napisał właśnie tę książkę: c2.com/cgi/wiki?WorkingEffectivelyWithLegacyCode W nim opisano szereg technik przełamywania trudnych zależności, dzięki czemu kod może stać się bardziej testowalny.
cbojar
2
Adapter wokół biblioteki innej firmy? Tak, dokładnie to polecam. Te testy jednostkowe nie poprawią Twojego kodu. Nie sprawią, że będzie bardziej niezawodny lub łatwiejszy w utrzymaniu. W tym momencie tylko częściowo kopiujesz kod innej osoby; w tym przypadku duplikujesz źle napisany kod z jego dźwięku. To strata netto. Niektóre odpowiedzi sugerują przeprowadzenie testów integracji; to dobry pomysł, jeśli chcesz tylko: „Czy to działa?” kontrola poczytalności. Dobre testowanie jest trudne i wymaga tyle samo umiejętności i intuicji, co dobrego kodu.
jpmc26,
4
Doskonała ilustracja zła wbudowanych. Dlaczego nie biblioteka zwrócić Timestampklasę (zawierający żadnej reprezentacji chcą) i zapewniają nazwanych metod ( .seconds(), .milliseconds(), .microseconds(), .nanoseconds()) i oczywiście nazwanych konstruktorów. Wtedy nie byłoby problemów.
Matthieu M.,
2
Przypomina sobie powiedzenie „wszystkie problemy w kodowaniu można rozwiązać za pomocą warstwy pośredniej (z wyjątkiem, oczywiście, problemu zbyt wielu warstw pośrednich)”
Dan Pantry

Odpowiedzi:

27

Wygląda na to, że już robisz należytą staranność. Ale ...

Na najbardziej praktycznym poziomie zawsze dołączaj do swojego zestawu garść testów integracyjnych z pełną pętlą dla własnego kodu i pisz więcej asercji, niż ci się wydaje. W szczególności powinieneś mieć garść testów, które wykonują pełny cykl create-read- [do_stuff] -validate.

[TestMethod]
public void MyFormatter_FormatsTimesCorrectly() {

  // this test isn't necessarily about the stream or the external interpreter.
  // but ... we depend on them working how we think they work:
  var stream = new StreamThingy();
  var interpreter = new InterpreterThingy(stream);
  stream.Write("id-123, some description, 12345");

  // this is what you're actually testing. but, it'll also hiccup
  // if your 3rd party dependencies introduce a breaking change.
  var formatter = new MyFormatter(interpreter);
  var line = formatter.getLine();
  Assert.equal(
    "some description took 123.45 seconds to complete (id-123)", line
  );
}

I wygląda na to, że już to robisz. Po prostu masz do czynienia z niestabilną i / lub skomplikowaną biblioteką. I w takim przypadku dobrze jest przedstawić kilka takich testów, które sprawdzają twoje rozumienie biblioteki i służą jako przykłady korzystania z biblioteki.

Załóżmy, że musisz zrozumieć i polegać na tym, jak parser JSON interpretuje każdy „typ” w łańcuchu JSON. Pomocne i trywialne jest umieszczanie czegoś takiego w swoim pakiecie:

[TestMethod]
public void JSONParser_InterpretsTypesAsExpected() {
  String datastream = "{nbr:11,str:"22",nll:null,udf:undefined}";
  var o = (new JSONParser()).parse(datastream);

  Assert.equal(11, o.nbr);
  Assert.equal(Int32.getType(), o.nbr.getType());
  Assert.equal("22", o.str);
  Assert.equal(null, o.nll);
  Assert.equal(Object.getType(), o.nll.getType());
  Assert.isFalse(o.KeyExists(udf));
}

Ale po drugie, pamiętaj, że zautomatyzowane testy dowolnego rodzaju i na prawie każdym poziomie rygorystyczności nadal nie ochronią cię przed wszystkimi błędami. Zupełnie powszechne jest dodawanie testów w miarę wykrycia problemów. Brak działu kontroli jakości oznacza, że ​​wiele z tych problemów zostanie odkrytych przez użytkowników końcowych.

I w znacznym stopniu jest to po prostu normalne.

I po trzecie, kiedy biblioteka zmienia znaczenie wartości zwracanej lub pola bez zmiany nazwy pola lub metody lub w inny sposób „łamania” kodu zależnego (może przez zmianę jego typu), byłbym cholernie niezadowolony z tego wydawcy. I twierdzę, że nawet jeśli prawdopodobnie powinieneś przeczytać dziennik zmian, jeśli taki istnieje, prawdopodobnie powinieneś również trochę stresować wydawcę. Twierdziłbym, że potrzebują konstruktywnej krytyki ...

svidgen
źródło
Ugh, chciałbym, żeby to było tak proste, jak wprowadzenie łańcucha jsona do biblioteki. To nie jest. Nie mogę zrobić odpowiednika (new JSONParser()).parse(datastream), ponieważ NetworkInterfacepobierają dane bezpośrednio z a, a wszystkie klasy, które wykonują rzeczywistą analizę, są pakietami prywatnymi i rozwijanymi.
durron597
Ponadto dziennik zmian nie uwzględnił faktu, że zmienili znaczniki czasu z ms na ns, wśród innych bólów głowy, których nie udokumentowali. Tak, jestem z nich bardzo niezadowolony i wyraziłem to im.
durron597,
@ durron597 Oh, prawie nigdy nie jest. Ale często można sfałszować podstawowe źródło danych - jak w pierwszym przykładzie kodu. ... Chodzi o to: w miarę możliwości wykonaj testy integracyjne w pełnej pętli, przetestuj swoją znajomość biblioteki, jeśli to możliwe, i po prostu pamiętaj, że nadal będziesz wpuszczać błędy na wolność. A zewnętrzni dostawcy muszą ponosić odpowiedzialność za wprowadzanie niewidocznych, przełomowych zmian.
svidgen,
@ durron597 Nie znam się na NetworkInterface... czy jest to coś, do czego można karmić dane, podłączając interfejs do portu na localhost czy coś takiego?
svidgen,
NetworkInterface. Jest to obiekt niskiego poziomu do bezpośredniej pracy z kartą sieciową i otwierania na niej gniazd itp.
durron597,
11

Krótka odpowiedź: to trudne. Prawdopodobnie masz wrażenie, że nie ma dobrych odpowiedzi, a to dlatego, że nie ma łatwych odpowiedzi.

Długa odpowiedź: jak mówi @ptyx , potrzebujesz testów systemowych i testów integracji, a także testów jednostkowych:

  • Testy jednostkowe są szybkie i łatwe do uruchomienia. Łapią błędy w poszczególnych sekcjach kodu i wykorzystują symulacje, aby umożliwić ich uruchomienie. Z konieczności nie mogą wychwycić rozbieżności między fragmentami kodu (np. W milisekundach w porównaniu do nanosekund).
  • Testy integracyjne i testy systemowe działają wolniej (er) i trudniej (er), ale wykrywają więcej błędów.

Kilka konkretnych sugestii:

  • Zaletą uruchomienia testu systemu w celu uzyskania jak największej ilości systemu jest pewna korzyść. Nawet jeśli nie jest w stanie potwierdzić większości zachowań lub bardzo dobrze wskazać problem. (Micheal Feathers omawia to bardziej w Efektywnej pracy ze starszym kodem ).
  • Inwestowanie w testowalność pomaga. Istnieje tutaj ogromna liczba technik: ciągła integracja, skrypty, maszyny wirtualne, narzędzia do odtwarzania, proxy lub przekierowywanie ruchu sieciowego.
  • Jedna z zalet (przynajmniej dla mnie) inwestowania w testowalność może być nieoczywista: jeśli testy są żmudne, denerwujące lub kłopotliwe w pisaniu lub uruchamianiu, to zbyt łatwo mi je pominąć, jeśli jestem pod presją lub zmęczony. Ważne jest utrzymywanie testów poniżej progu „To takie proste, że nie ma usprawiedliwienia, aby tego nie robić”.
  • Idealne oprogramowanie nie jest możliwe. Podobnie jak wszystko inne, wysiłek poświęcony na testowanie jest kompromisem, a czasem nie jest wart wysiłku. Istnieją ograniczenia (takie jak brak działu kontroli jakości). Zaakceptuj, że pojawią się błędy, odzyskaj i naucz się.

Widziałem programowanie opisane jako czynność uczenia się o problemie i przestrzeni rozwiązań. Osiągnięcie perfekcji z wyprzedzeniem może nie być możliwe, ale możesz się uczyć po fakcie. („Poprawiłem obsługę znaczników czasu w kilku miejscach, ale pominąłem jedno. Czy mogę zmienić typy danych lub klasy, aby uczynić obsługę znaczników czasu bardziej wyraźnym i trudniejszym do pominięcia, lub też uczynić ją bardziej scentralizowaną, aby mieć tylko jedno miejsce do zmiany? Czy mogę modyfikować moje testy w celu zweryfikowania większej liczby aspektów obsługi znaczników czasu? Czy mogę uprościć środowisko testowe, aby ułatwić to w przyszłości? Czy mogę sobie wyobrazić jakieś narzędzie, które by to ułatwiło, a jeśli tak, to czy mogę znaleźć takie narzędzie w Google? „Itd.)

Josh Kelley
źródło
7

Zaktualizowałem wersję biblioteki…, która… spowodowała zmianę znaczników czasu (zwracanych przez bibliotekę zewnętrzną jako long) z milisekund po epoce na nanosekundy po epoce.

To nie jest błąd w bibliotece

Zdecydowanie się z tobą nie zgadzam. Jest to błąd w bibliotece , w rzeczywistości dość podstępny. Zmienili semantyczny typ zwracanej wartości, ale nie zmienili programowego typu zwracanej wartości. Może to spowodować wszelkiego rodzaju spustoszenie, zwłaszcza jeśli był to niewielki guz wersji, ale nawet jeśli był poważny.

Powiedzmy, że zamiast tego biblioteka zwróciła rodzaj MillisecondsSinceEpochprostego opakowania zawierającego long. Gdy zmienili go na NanosecondsSinceEpochwartość, Twój kod nie skompilowałby się i oczywiście wskazałby ci miejsca, w których musisz dokonać zmian. Zmiana nie mogła po cichu uszkodzić twojego programu.

Jeszcze lepiej byłby TimeSinceEpochobiekt, który mógłby dostosować interfejs, ponieważ dodano większą precyzję, na przykład dodając #toLongNanosecondsmetodę obok #toLongMillisecondsmetody, nie wymagającą żadnych zmian w kodzie.

Kolejnym problemem jest to, że nie masz niezawodnego zestawu testów integracyjnych w bibliotece. Powinieneś to napisać. Lepiej byłoby stworzyć interfejs wokół tej biblioteki, aby obudować ją z dala od reszty aplikacji. Rozwiązuje to kilka innych odpowiedzi (a kolejne pojawiają się podczas pisania). Testy integracyjne powinny być uruchamiane rzadziej niż testy jednostkowe. Właśnie dlatego posiadanie warstwy buforowej pomaga. Pogrupuj testy integracyjne w osobny obszar (lub nazwij je inaczej), abyś mógł je uruchomić w razie potrzeby, ale nie za każdym razem, gdy przeprowadzasz test jednostkowy.

Cbojar
źródło
2
@ durron597 Nadal twierdzę, że to błąd. Oprócz braku dokumentacji, po co w ogóle zmieniać oczekiwane zachowanie? Dlaczego nie nowa metoda, która zapewnia nową precyzję i pozwala starej metodzie nadal dostarczać millis? A dlaczego nie zapewnić kompilatorowi sposobu powiadamiania Cię o zmianie typu zwrotu? Nie trzeba wiele, aby uczynić to bardziej zrozumiałym, nie tylko w dokumentacji, ale w samym kodzie.
cbojar
1
@gbjbaanb, „że mają złe praktyki wydawania” wydaje mi się błędem
Arturo Torres Sánchez
2
@gbjbaanb Biblioteka innej firmy [powinna] zawrzeć „umowę” ze swoimi użytkownikami. Zerwanie umowy - niezależnie od tego, czy jest udokumentowane, czy nie - może / powinno być uważane za błąd. Jak mówili inni, jeśli trzeba coś zmienić, dodać do umowy z nowej funkcji / metody (por wszystkich ...Ex()metod w Win32API). Jeśli nie jest to możliwe, „zerwanie” kontraktu poprzez zmianę nazwy funkcji (lub jej typu zwracanego) byłoby lepsze niż zmiana zachowania.
TripeHound,
1
To jest błąd w bibliotece. Długie używanie nanosekund to pchanie.
Joshua
1
@gbjbaanb Mówisz, że nie jest to błąd, ponieważ jest to zamierzone zachowanie, nawet jeśli jest nieoczekiwane. W tym sensie nie jest to błąd implementacyjny , ale taki sam błąd. Może to być nazywane wadą projektową lub błędem interfejsu . Wady polegają na tym, że ujawniają prymitywną obsesję na punkcie długich, a nie wyraźnych jednostek, ich abstrakcja jest nieszczelna, ponieważ eksportuje szczegóły dotyczące jej wewnętrznej implementacji (dane są przechowywane jako długie określonej jednostki) i że narusza zasada najmniejszego zdziwienia z subtelną zmianą jednostki.
cbojar
5

Potrzebujesz integracji i testów systemowych.

Testy jednostkowe wspaniale sprawdzają, czy kod zachowuje się zgodnie z oczekiwaniami. Jak zdajesz sobie sprawę, nic nie podważa twoich założeń lub zapewnia, że ​​twoje oczekiwania są zdrowe.

O ile twój produkt nie ma niewielkiej interakcji z systemami zewnętrznymi lub nie wchodzi w interakcje z systemami tak dobrze znanymi, stabilnymi i udokumentowanymi, że można je śmiało wyśmiewać (rzadko zdarza się to w prawdziwym świecie) - testy jednostkowe nie są wystarczające.

Im wyższy poziom testów, tym bardziej będą cię chronić przed nieoczekiwanym. Jest to kosztowne (wygoda, szybkość, kruchość ...), więc testy jednostkowe powinny pozostać fundamentem twoich testów, ale potrzebujesz innych warstw, w tym - w końcu - odrobiny testów na ludziach, które mają długą drogę do złapania głupie rzeczy, o których nikt nie myślał.

ptyx
źródło
2

Najlepiej byłoby stworzyć minimalny prototyp i zrozumieć, jak dokładnie działa biblioteka. W ten sposób zyskasz trochę wiedzy o bibliotece ze słabą dokumentacją. Prototyp może być minimalistycznym programem, który korzysta z tej biblioteki i wykonuje funkcje.

W przeciwnym razie nie ma sensu pisać testów jednostkowych z półokreślonymi wymaganiami i słabym zrozumieniem systemu.

Jeśli chodzi o konkretny problem - dotyczący używania niewłaściwych wskaźników: potraktowałbym to jako zmianę wymagań. Po rozpoznaniu problemu zmień testy jednostkowe i kod.

BЈовић
źródło
1

Jeśli korzystasz z popularnej, stabilnej biblioteki, możesz być może założyć, że nie zagra ona na ciebie paskudnych sztuczek. Ale jeśli rzeczy takie jak to, co opisałeś, zdarzają się w tej bibliotece, to oczywiście nie jest to jedno. Po tym złym doświadczeniu za każdym razem, gdy coś pójdzie nie tak podczas interakcji z biblioteką, będziesz musiał zbadać nie tylko możliwość popełnienia błędu, ale także możliwość popełnienia błędu przez bibliotekę. Powiedzmy, że jest to biblioteka „niepewna”.

Jedną z technik stosowanych w bibliotekach, co do których „nie jesteśmy pewni”, jest zbudowanie warstwy pośredniej między naszym systemem a wymienionymi bibliotekami, co wyodrębnia funkcjonalność bibliotek, potwierdza, że ​​nasze oczekiwania względem biblioteki są słuszne, a także znacznie upraszcza w naszym życiu w przyszłości, jeśli zdecydujemy się na uruchomienie tej biblioteki i zastąpienie jej inną biblioteką, która zachowuje się lepiej.

Mike Nakis
źródło
To tak naprawdę nie odpowiada na pytanie. Mam już warstwę oddzielającą bibliotekę od mojego systemu, ale problem polega na tym, że moja warstwa abstrakcji może zawierać „błędy”, gdy biblioteka zmienia się na mnie bez ostrzeżenia.
durron597,
1
@ durron597 Może warstwa nie izoluje w wystarczającym stopniu biblioteki od reszty aplikacji. Jeśli okaże się, że masz trudności z testowaniem tej warstwy, być może musisz uprościć zachowanie i ściślej izolować dane bazowe.
cbojar
Co powiedział @cbojar. Powtórzę też coś, co mogło pozostać niezauważone w powyższym tekście: Słowo assertkluczowe (lub funkcja lub funkcja, w zależności od używanego języka) jest Twoim przyjacielem. Nie mówię o asercjach w testach jednostkowych / integracyjnych, mówię, że warstwa izolacyjna powinna być bardzo ciężka z asercjami, zapewniając wszystko, co można domagać się na temat zachowania biblioteki.
Mike Nakis,
Te twierdzenia niekoniecznie wykonują się podczas uruchomień produkcyjnych, ale wykonują się podczas testowania, mając białe pole widzenia twojej warstwy izolacyjnej, a zatem będąc w stanie upewnić się (w miarę możliwości), że informacje, które warstwa otrzymuje z biblioteki jest zdrowy.
Mike Nakis,