Kod testów jednostkowych z zależnością od systemu plików

138

Piszę komponent, który mając plik ZIP, musi:

  1. Rozpakuj plik.
  2. Znajdź konkretną bibliotekę dll wśród rozpakowanych plików.
  3. Załaduj tę bibliotekę dll przez odbicie i wywołaj na niej metodę.

Chciałbym przetestować jednostkowo ten komponent.

Kusi mnie, aby napisać kod, który zajmuje się bezpośrednio systemem plików:

void DoIt()
{
   Zip.Unzip(theZipFile, "C:\\foo\\Unzipped");
   System.IO.File myDll = File.Open("C:\\foo\\Unzipped\\SuperSecret.bar");
   myDll.InvokeSomeSpecialMethod();
}

Ale ludzie często mówią: „Nie pisz testów jednostkowych, które opierają się na systemie plików, bazie danych, sieci itp.”

Gdybym napisał to w sposób przyjazny dla testów jednostkowych, przypuszczam, że wyglądałoby to tak:

void DoIt(IZipper zipper, IFileSystem fileSystem, IDllRunner runner)
{
   string path = zipper.Unzip(theZipFile);
   IFakeFile file = fileSystem.Open(path);
   runner.Run(file);
}

Yay! Teraz jest to testowalne; Mogę karmić testowymi dublami (próbami) metodą DoIt. Ale jakim kosztem? Musiałem teraz zdefiniować 3 nowe interfejsy, aby było to testowalne. A co dokładnie testuję? Testuję, czy mój DoIt działa poprawnie z jego zależnościami. Nie sprawdza, czy plik zip został poprawnie rozpakowany itp.

Nie wydaje mi się, żebym już testował funkcjonalność. Mam wrażenie, że właśnie testuję interakcje w klasie.

Moje pytanie brzmi : jaki jest właściwy sposób testowania jednostkowego czegoś, co jest zależne od systemu plików?

edytuj Używam .NET, ale koncepcja może również zastosować Java lub kod natywny.

Judah Gabriel Himango
źródło
8
Ludzie mówią, że nie zapisuj do systemu plików w teście jednostkowym, ponieważ jeśli masz ochotę pisać do systemu plików, nie rozumiesz, co stanowi test jednostkowy. Test jednostkowy zwykle wchodzi w interakcję z pojedynczym rzeczywistym obiektem (testowaną jednostką), a wszystkie inne zależności są mockowane i przekazywane. Klasa testowa składa się następnie z metod testowych, które sprawdzają ścieżki logiczne poprzez metody obiektu i TYLKO ścieżki logiczne w badanej jednostki.
Christopher Perry,
1
w twojej sytuacji jedyną częścią, która wymaga testów jednostkowych myDll.InvokeSomeSpecialMethod();, byłoby sprawdzenie, czy działa poprawnie zarówno w sytuacjach sukcesu, jak i niepowodzenia, więc nie przeprowadzałbym testów jednostkowych, DoItale DllRunner.Runpowiedziałbym, że niewłaściwe użycie testu UNIT do podwójnego sprawdzenia, czy cały proces działa, byłoby dopuszczalne niewłaściwe użycie i ponieważ byłby to test integracyjny
udający

Odpowiedzi:

47

Naprawdę nie ma w tym nic złego, to tylko kwestia tego, czy nazwiesz to testem jednostkowym czy testem integracji. Musisz tylko upewnić się, że w przypadku interakcji z systemem plików nie wystąpią żadne niezamierzone skutki uboczne. W szczególności upewnij się, że posprzątałeś po sobie - usuń wszystkie utworzone pliki tymczasowe - i nie nadpisujesz przypadkowo istniejącego pliku, który miał taką samą nazwę jak plik tymczasowy, którego używasz. Zawsze używaj ścieżek względnych, a nie ścieżek bezwzględnych.

Dobrym pomysłem byłoby również chdir()przejście do katalogu tymczasowego przed uruchomieniem testu i chdir()później.

Adam Rosenfield
źródło
27
+1, jednak pamiętaj, że chdir()obejmuje cały proces, więc możesz zepsuć możliwość równoległego uruchamiania testów, jeśli Twoja platforma testowa lub przyszła jej wersja to obsługują.
69

Yay! Teraz jest to testowalne; Mogę karmić testowymi dublami (próbami) metodą DoIt. Ale jakim kosztem? Musiałem teraz zdefiniować 3 nowe interfejsy, aby było to testowalne. A co dokładnie testuję? Testuję, czy mój DoIt działa poprawnie z jego zależnościami. Nie sprawdza, czy plik zip został poprawnie rozpakowany itp.

Trafiłeś w gwóźdź prosto w jego głowę. To, co chcesz przetestować, to logika metody, niekoniecznie to, czy można zaadresować prawdziwy plik. Nie musisz testować (w tym teście jednostkowym), czy plik jest poprawnie rozpakowany, Twoja metoda przyjmuje to za pewnik. Interfejsy są cenne same w sobie, ponieważ zapewniają abstrakcje, względem których można programować, zamiast pośrednio lub jawnie polegać na jednej konkretnej implementacji.

andreas buykx
źródło
12
Testowalna DoItfunkcja, jak podano, nie wymaga nawet testowania. Jak słusznie zauważył pytający, nie ma już nic istotnego do sprawdzenia. Teraz to realizacja IZipper, IFileSystemi IDllRunnerże potrzeby testowania, ale one są właśnie rzeczy, które zostały wyśmiewali się do testu!
Ian Goldby,
56

Twoje pytanie odsłania jedną z najtrudniejszych części testowania dla programistów, którzy dopiero się w to wkręcają:

- Co do cholery mam testować?

Twój przykład nie jest zbyt interesujący, ponieważ po prostu skleja ze sobą niektóre wywołania API, więc gdybyś napisał dla niego test jednostkowy, zakończyłbyś po prostu stwierdzeniem, że metody zostały wywołane. Takie testy ściśle wiążą szczegóły implementacji z testem. To jest złe, ponieważ teraz musisz zmieniać test za każdym razem, gdy zmieniasz szczegóły implementacji metody, ponieważ zmiana szczegółów implementacji psuje test (y)!

Posiadanie złych testów jest w rzeczywistości gorsze niż brak testów.

W twoim przykładzie:

void DoIt(IZipper zipper, IFileSystem fileSystem, IDllRunner runner)
{
   string path = zipper.Unzip(theZipFile);
   IFakeFile file = fileSystem.Open(path);
   runner.Run(file);
}

Chociaż możesz przekazać fałszywe informacje, nie ma logiki w metodzie do przetestowania. Gdybyś spróbował wykonać w tym celu test jednostkowy, może to wyglądać mniej więcej tak:

// Assuming that zipper, fileSystem, and runner are mocks
void testDoIt()
{
  // mock behavior of the mock objects
  when(zipper.Unzip(any(File.class)).thenReturn("some path");
  when(fileSystem.Open("some path")).thenReturn(mock(IFakeFile.class));

  // run the test
  someObject.DoIt(zipper, fileSystem, runner);

  // verify things were called
  verify(zipper).Unzip(any(File.class));
  verify(fileSystem).Open("some path"));
  verify(runner).Run(file);
}

Gratulacje, po prostu skopiowałeś szczegóły implementacji swojej DoIt()metody do testu. Miłego utrzymania.

Pisząc testy, chcesz przetestować CO, a nie JAK . Zobacz Testowanie czarnoskrzynkowe uzyskać więcej informacji, .

CO jest nazwa metody (a przynajmniej powinien być). JAK są małe szczegóły implementacji, które żyją wewnątrz metody. Dobre testy pozwalają zamienić JAK bez przerywania CO .

Pomyśl o tym w ten sposób, zadaj sobie pytanie:

„Jeśli zmienię szczegóły implementacji tej metody (bez zmiany zamówienia publicznego), czy spowoduje to przerwanie mojego testu (-ów)?”

Jeśli odpowiedź brzmi tak, testujesz JAK, a nie CO .

Aby odpowiedzieć na konkretne pytanie dotyczące testowania kodu z zależnościami systemu plików, powiedzmy, że miałeś coś bardziej interesującego z plikiem i chciałeś zapisać zawartość a zakodowaną w Base64 byte[]do pliku. Możesz użyć do tego strumieni, aby sprawdzić, czy Twój kod działa prawidłowo, bez konieczności sprawdzania, jak to robi. Przykładem może być coś takiego (w Javie):

interface StreamFactory {
    OutputStream outStream();
    InputStream inStream();
}

class Base64FileWriter {
    public void write(byte[] contents, StreamFactory streamFactory) {
        OutputStream outputStream = streamFactory.outStream();
        outputStream.write(Base64.encodeBase64(contents));
    }
}

@Test
public void save_shouldBase64EncodeContents() {
    OutputStream outputStream = new ByteArrayOutputStream();
    StreamFactory streamFactory = mock(StreamFactory.class);
    when(streamFactory.outStream()).thenReturn(outputStream);

    // Run the method under test
    Base64FileWriter fileWriter = new Base64FileWriter();
    fileWriter.write("Man".getBytes(), streamFactory);

    // Assert we saved the base64 encoded contents
    assertThat(outputStream.toString()).isEqualTo("TWFu");
}

Test wykorzystuje ByteArrayOutputStreamjednak w aplikacji (za pomocą iniekcji zależność) prawdziwy StreamFactory (chyba nazywa FileStreamFactory) wróci FileOutputStreamz outputStream()i by napisać do File.

To, co było interesujące w tej writemetodzie, to fakt, że zapisywała zawartość zakodowaną w Base64, więc właśnie to testowaliśmy. W przypadku Twojej DoIt()metody byłoby to lepiej przetestowane za pomocą testu integracji .

Christopher Perry
źródło
1
Nie jestem pewien, czy zgadzam się z Twoją wiadomością tutaj. Czy chcesz powiedzieć, że nie ma potrzeby przeprowadzania testów jednostkowych tego rodzaju metody? Więc zasadniczo mówisz, że TDD jest złe? Tak jakbyś robił TDD, nie możesz napisać tej metody bez uprzedniego napisania testu. Czy możesz polegać na przeczuciu, że Twoja metoda nie będzie wymagała testu? Powodem, dla którego WSZYSTKIE frameworki testów jednostkowych zawierają funkcję „weryfikacji”, jest to, że można jej używać. „To źle, ponieważ teraz musisz zmieniać test za każdym razem, gdy zmieniasz szczegóły implementacji swojej metody” ... witamy w świecie testów jednostkowych.
Ronnie
2
Powinieneś przetestować KONTRAKT metody, a nie jej implementację. Jeśli musisz zmieniać test za każdym razem, gdy implementacja tego kontraktu się zmienia, to czeka Cię straszny czas utrzymywania zarówno bazy kodu aplikacji, jak i bazy kodu testowego.
Christopher Perry,
@Ronnie ślepe stosowanie testów jednostkowych nie jest pomocne. Istnieją projekty o bardzo zróżnicowanym charakterze, a testy jednostkowe nie są skuteczne we wszystkich z nich. Jako przykład, pracuję nad projektem, w którym 95% kodu jest o skutki uboczne (uwaga, ten efekt uboczny ciężki charakter jest o wymogu , to niezbędna złożoność, a nie przypadkowe , ponieważ gromadzi dane z szeroką gamę źródeł stanowych i przedstawia je przy niewielkiej manipulacji, więc nie ma prawie żadnej czystej logiki). Testowanie jednostkowe nie jest tutaj skuteczne, testowanie integracyjne jest.
Vicky Chijwani,
Efekty uboczne należy zepchnąć na krawędzie systemu, nie powinny one przeplatać się między warstwami. Na krawędziach testujesz efekty uboczne, czyli zachowania. Wszędzie indziej powinieneś starać się mieć czyste funkcje bez skutków ubocznych, które można łatwo przetestować i łatwo uzasadnić, ponownie wykorzystać i skomponować.
Christopher Perry,
24

Nie mam zamiaru zanieczyszczać mojego kodu typami i koncepcjami, które istnieją tylko w celu ułatwienia testowania jednostkowego. Jasne, jeśli dzięki temu projekt będzie czystszy i lepszy, to świetny, ale myślę, że często tak nie jest.

Uważam, że testy jednostkowe zrobiłyby tyle, ile mogą, co może nie być 100% pokryciem. W rzeczywistości może to być tylko 10%. Chodzi o to, że testy jednostkowe powinny być szybkie i nie mieć żadnych zewnętrznych zależności. Mogą testować przypadki, takie jak „ta metoda zgłasza ArgumentNullException po przekazaniu wartości null dla tego parametru”.

Następnie dodałbym testy integracyjne (również zautomatyzowane i prawdopodobnie przy użyciu tej samej struktury testów jednostkowych), które mogą mieć zewnętrzne zależności i testować scenariusze kompleksowe, takie jak te.

Podczas pomiaru pokrycia kodu mierzę zarówno testy jednostkowe, jak i integracyjne.

Kent Boogaart
źródło
5
Tak, słyszę cię. Jest taki dziwaczny świat, do którego docierasz, gdzie odłączyłeś się tak bardzo, że cała twoja reszta to wywołania metod na abstrakcyjnych obiektach. Przewiewny puch. Kiedy osiągasz ten punkt, nie wydaje się, że naprawdę testujesz coś prawdziwego. Po prostu testujesz interakcje między klasami.
Judah Gabriel Himango,
6
Ta odpowiedź jest błędna. Testowanie jednostkowe to nie lukier, bardziej przypomina cukier. Jest upieczony w cieście. To część pisania kodu ... działanie projektowe. Dlatego nigdy nie „zanieczyszczasz” swojego kodu niczym, co „ułatwiłoby testowanie”, ponieważ testowanie ułatwia pisanie kodu. W 99% przypadków test jest trudny do napisania, ponieważ programista napisał kod przed testem, a skończył na pisaniu złego, niemożliwego
Christopher Perry,
1
@Christopher: aby rozszerzyć twoją analogię, nie chcę, aby moje ciasto przypominało kawałek wanilii, tylko po to, abym mógł użyć cukru. Wszystko, za czym się opowiadam, to pragmatyzm.
Kent Boogaart
1
@Christopher: Twoja biografia mówi wszystko: „Jestem fanatykiem TDD”. Z drugiej strony jestem pragmatyczny. Robię TDD tam, gdzie pasuje, a nie tam, gdzie nie - nic w mojej odpowiedzi nie sugeruje, że nie robię TDD, chociaż wydaje ci się, że tak. I bez względu na to, czy jest to TDD, czy nie, nie będę wprowadzać dużej złożoności, aby ułatwić testowanie.
Kent Boogaart
3
@ChristopherPerry Czy możesz wyjaśnić, jak rozwiązać pierwotny problem OP w sposób TDD? Ciągle na to wpadam; Muszę napisać funkcję, której jedynym celem jest wykonanie akcji z zewnętrzną zależnością, jak w tym pytaniu. Więc nawet w scenariuszu „napisz test najpierw”, co by to było za test?
Dax Fohl
8

Nie ma nic złego w trafieniu do systemu plików, po prostu potraktuj to jako test integracji, a nie test jednostkowy. Zamieniłbym trwale zakodowaną ścieżkę na ścieżkę względną i utworzyłbym podfolder TestData, aby zawierał suwaki dla testów jednostkowych.

Jeśli testy integracji trwają zbyt długo, oddziel je, aby nie były uruchamiane tak często, jak szybkie testy jednostkowe.

Zgadzam się, czasami myślę, że testowanie oparte na interakcji może powodować zbyt wiele sprzężeń i często kończy się niewystarczającą wartością. Naprawdę chcesz tutaj przetestować rozpakowywanie pliku, a nie tylko sprawdzić, czy wywołujesz właściwe metody.

JC.
źródło
To, jak często biegają, nie ma większego znaczenia; używamy serwera ciągłej integracji, który automatycznie je uruchamia. Nie obchodzi nas, jak długo to potrwa. Jeśli „jak długo działać” nie jest problemem, czy jest jakiś powód, aby odróżniać testy jednostkowe od testów integracyjnych?
Judah Gabriel Himango,
4
Nie całkiem. Ale jeśli programiści chcą szybko uruchomić wszystkie testy jednostkowe lokalnie, dobrze jest mieć na to łatwy sposób.
JC.
6

Jednym ze sposobów byłoby napisanie metody unzip, która pobierze InputStreams. Następnie test jednostkowy mógłby skonstruować taki InputStream z tablicy bajtów przy użyciu ByteArrayInputStream. Zawartość tej tablicy bajtów może być stałą w kodzie testu jednostkowego.

nsayer
źródło
Ok, to pozwala na wtrysk strumienia. Wstrzykiwanie zależności / IOC. A co z rozpakowaniem strumienia do plików, załadowaniem biblioteki dll między tymi plikami i wywołaniem metody w tej bibliotece dll?
Judah Gabriel Himango
3

Wydaje się, że jest to bardziej test integracji, ponieważ polegasz na konkretnym szczególe (systemie plików), który teoretycznie może się zmienić.

Abstrahowałbym kod, który zajmuje się systemem operacyjnym, do jego własnego modułu (klasa, zespół, jar, cokolwiek). W twoim przypadku chcesz załadować konkretną bibliotekę DLL, jeśli zostanie znaleziona, więc utwórz interfejs IDllLoader i klasę DllLoader. Niech Twoja aplikacja pobierze bibliotekę DLL z DllLoader za pomocą interfejsu i przetestuj to… nie jesteś jednak odpowiedzialny za rozpakowywanie kodu, prawda?

kran
źródło
2

Zakładając, że „interakcje systemu plików” są dobrze przetestowane w samym frameworku, utwórz metodę do pracy ze strumieniami i przetestuj ją. Otwarcie FileStream i przekazanie go do metody można pominąć w testach, ponieważ FileStream.Open jest dobrze przetestowany przez twórców frameworka.

Sunny Milenov
źródło
Ty i nsayer macie zasadniczo tę samą sugestię: spraw, aby mój kod działał ze strumieniami. A co z częścią dotyczącą rozpakowania zawartości strumienia do plików dll, otwarcia tej biblioteki i wywołania w niej funkcji? Co byś tam zrobił?
Judah Gabriel Himango,
3
@JudahHimango. Te części niekoniecznie muszą być testowalne. Nie możesz wszystkiego przetestować. Podziel niemożliwe do przetestowania komponenty we własne bloki funkcjonalne i załóż, że będą działać. Kiedy napotkasz błąd w działaniu tego bloku, opracuj dla niego test i voila. Testowanie jednostkowe NIE oznacza, że ​​musisz wszystko testować. W niektórych scenariuszach pokrycie kodu w 100% jest nierealne.
Zoran Pavlovic
1

Nie należy testować interakcji klas i wywołań funkcji. zamiast tego powinieneś rozważyć testy integracyjne. Przetestuj wymagany wynik, a nie operację ładowania pliku.

Dror Helper
źródło
1

W przypadku testów jednostkowych sugerowałbym włączenie pliku testowego do projektu (plik EAR lub odpowiednik), a następnie użycie ścieżki względnej w testach jednostkowych, tj. „../Testdata/testfile”.

Tak długo, jak projekt jest poprawnie eksportowany / importowany, test jednostkowy powinien działać.

James Anderson
źródło
0

Jak powiedzieli inni, pierwszy jest w porządku jako test integracji. Druga sprawdza tylko to, co funkcja ma faktycznie robić, czyli wszystko, co powinien zrobić test jednostkowy.

Jak pokazano, drugi przykład wygląda trochę bez sensu, ale daje możliwość sprawdzenia, jak funkcja reaguje na błędy w którymkolwiek z kroków. W przykładzie nie ma żadnego sprawdzania błędów, ale w prawdziwym systemie możesz mieć, a wstrzyknięcie zależności pozwoli ci przetestować wszystkie odpowiedzi na wszelkie błędy. Wtedy koszt będzie tego wart.

David Sykes
źródło