niedziela, 18 marca 2012

"Teoria mostów" czyli dlaczego programiści mylą testy jednostkowe z integracyjnymi

Na początek quiz : po jakim czasie programiści zaczynają z coraz większą częstością stosować dobrze znane polecenie?


-Dmaven.test.skip=true

Prowadząc osobiste obserwacje zauważyłem, że jednym z czynników jaki ma wpływ na ów niechlubny krok jest czas wykonania się testów jednostkowych. Jeśli do kogoś bardziej przemawiają zapisy ścisłe to można powyższe stwierdzenie ująć TAK:


P(s)~Tc

gdzie:
  • P(s) - prawdopodobieństwo przeskoczenia kroku testów
  • Tc - czas wykonania się testów

lub TAK:

f(s)~Tc

gdzie:
  • f(s) - częstotliwość olewania testów
  • Tc - czas wykonania się testów

Wniosek jest oczywisty : wśród rzeczy które w naszym codziennym życiu chcemy sobie wydłużać nie powinno być testów jednostkowych!

Dlaczego programiści mylą testy jednostkowe z integracyjnymi


Oczywiście nie wszyscy programiści i na pewno nie ci którzy czytają tego bloga. Niestety świat zewnętrzny jest bezlitosny i nie raz widziałem jak coś co było w danej firmie nazywane "testami jednostkowymi" stawiało cały serwer aplikacji. Źródeł podobnych kroków dopatruję się w fakcie, iż tak naprawdę nie ma (niestety) ścisłej definicji testu jednostkowego. No i różni ludzie różnie rozumieją ową "jednostkę".

Zamiast ścisłej definicji zafundujmy sobie wolne i pozbawione ciśnień rozważania przy pomocy

prostego przykładu (z użyciem pseudo kodu)


 
 int countLines(pathToFile){
    File fileWithContent=new File(pathToFile);  
    String content=readContent( fileWithContent)  
    return countLines(content) 
 } 

Powyższy kod przedstawia kawałek logiki zliczającej ilość linii w pliku. Stworzenie testu jednostkowego dla tego kawałka kodu wydaje się proste.Niechże wygląda on tak:


 //given  
 pathToTestFile="..."  
 //when  
 int result=linesCounter.countLines(pathToTestFile)  
 //then  
 assertThat(result, is(tyleATyle))  

Dlaczego to nie jest test jednostkowy?


Aby odpowiedzieć na to pytanie wystarczy w dowolny sposób popsuć plik testowy. I tutaj niestety jest miejsce na filozoficzne dywagacje czy plik jest częścią testowanej jednostki czy też nie. Według mnie nie jest. Jako jednostkę rozumiem logikę znajdującą się w kontekście testowanego kawałka kodu. Usuwając plik testowy nie zmieniamy nic w samym komponencie ani w naszym teście a jednak jesteśmy w stanie zmienić kolor paska rezultatu w JUnicie. Moim zdaniem można śmiało powiedzieć, że nasz test poza liczeniem linii testuje integrację z systemem plików!


Mosty i "Kontektory"



Idea przedstawiona na rysunku jest chyba oczywista. Do każdego zewnętrznego systemu odwołujemy się poprzez specjalny zbiór łączników zwanych z angielska "connectorami" (chciałbym dożyć chwili gdy będąc w samym centrum tego pięknego kraju będę mógł ochoczo nazwać sobie klasę FileSystemŁącznik). Na początku używałem jednego kontektora dla jednego systemu ale skutkowało to rozrostem jednej ogromnej klasy. Najnowsza teoria grupuje kontektory w mosty.


Jak to może wyglądać w praktyce


Widok od strony pakietów


i przykładowy kod źródłowy

 public class TextResourcesConnector {  
      public String readTextResourceContent(String resourceName) {  
           InputSupplier<InputStream> contentSupplier = newInputStreamSupplier(getResource(resourceName));  
           return readContent(contentSupplier);  
      }  
      public String readTextFromFile(File file){  
           InputSupplier<? extends InputStream> contentSupplier =Files.newInputStreamSupplier(file);  
           return readContent(contentSupplier);  
      }  
      public String readTextFromFile(String filePath){  
           return readTextFromFile(new File(filePath));  
      }  
      private String readContent(InputSupplier<? extends InputStream> contentSupplier) {  
           try {  
                return CharStreams.toString(newReaderSupplier(contentSupplier, Charsets.UTF_8));  
           } catch (IOException e) {  
                throw new RuntimeException(e);  
           }  
      }  
 }  

Powyższy kod przedstawia wersję jednego z kontektorów w kształcie odpowiednim na chwilę obecną (co bardziej spotrzegawczy zauważą wykorzystanie Guavy). Koncepcja ewoluuje, więc nie wykluczone, że w przyszłość całość będzie wyglądać inaczej. Kod naszego "zliczacza linii" wyglądałby teraz następująco


 
 int countLines(pathToFile){ 
    String content=textResourcesConnector.readContent( fileWithContent)  
    return countLines(content) 
 } 

Zaś test :

 

 before(){
   textResourcesConnector=mock()
   linesCounter.wstrzyknijTakLubInaczej(textResourcesConnector)
 }

 (...)

 //given  
 pathToTestFile="..."
 content="..."
 given(textResourcesConnector.readContent(pathToTestFile)).willReturn(content);
 //when  
 int result=linesCounter.countLines(pathToTestFile)  
 //then  
 assertThat(result, is(tyleATyle))  

Test co prawda nam się powiększył objętościowy ale to dlatego, że teraz mamy pełną kontrolę nad tym co dzieje się na granicy systemu i z kontroli tej (a jakże) korzystamy. Oczywiście testowanie samych kontektorów to osobna sprawa - tutaj raczej przed testem integracyjnym się nie ucieknie.ALE!


Akapit-wniosek artykułu


Testy integracyjne poza tym, że są mniej przewidywalne to zazwyczaj trwają dłużej z powodu komunikacji dwóch systemów. Także aby ludzie nie "skipowali" testów to te integracyjne powinny być uruchamiane osobno od jednostkowych. W tej chwili osobiście używam mechanizmu Springowego

@IfProfileValue(name="test.type", value="integration")

Dzięki czemu owe testy integracyjne nie będą się uruchamiały razem z jednostkowymi. (Podobno maven 3 ma jakaś osobną fazę poświęconą testom integracyjnym ale ja jeszcze tego nie znaju.) Teraz gdy już mamy naszą annotację nad testami integracyjnymi to możemy stworzyć osobny job w jenkinsie, który raz na jakiś czas będzie je uruchamiał a programiści mogą radośnie delektować się (relatywnie) szybkim wykonaniem instukcji mvn clean install.

Ps. Do testowania operacji na systemie plików polecam w miarę młody mechanizm w JUnit

@Rule
public TemporaryFolder tmpFolder = new TemporaryFolder();


5 komentarzy:

  1. Dzięki za artykuł, choć z drugiej strony testy jednostkowe pisane tak ortodoksyjnie potrafią być niezłym bólem w ...

    Wolałbym już napisać test integracyjny z systemem plików, ale teraz sam płaczę, że testy mi wolno chodzą :-( Więc chyba jeszcze raz muszę przeczytać i się nad sobą zastanowić :-]

    PS. A Maven2 to nie miał osobnej fazy integration-test ? Oj chyba miał i ma :-)

    OdpowiedzUsuń
  2. No faktycznie maven2 ma fazę testów integracyjnych :)

    Z tego co widzę jest ona jednak przed "install".
    Wydaje mi się, że gdyby użyć tejże fazy w aplikacji wielo-modułowej gdzie trzeba zbudować i zainstalować w lokalnym repo jakieś zależności to te testy strasznie zamulą cały proces. I obawiam się, że skutek będzie taki, iż prędzej czy później ludzie zaczną zlewać wszystkie testy.

    OdpowiedzUsuń
  3. Testy integracyjne można bardzo łatwo oddzielić od jednostkowych mechanizmami Mavena. Opisałem to niedawno u siebie na blogu: http://maciejwalkowiak.pl/blog/2012/03/22/integration-tests-with-maven-3-failsafe-and-cargo-plugin/

    OdpowiedzUsuń
  4. Wygląda obiecująco, dzięki za link!

    OdpowiedzUsuń
  5. O ile dobrze rozumiem intencję, to architektura, o której piszesz ma nazwę Porta&Adapters - jest rozwinięciem starszej koncepcji Architektury Heksagonalnej.

    OdpowiedzUsuń