Testowanie niepublicznych metod w .NET

.NET, C#, Testy jednostkowe, top5

O tym, że automatyczne testowanie kodu za pomocą testów jednostkowych jest czynnością wartościową nie trzeba chyba współcześnie już nikogo przekonywać. Oczywiście w omawianym temacie istnieją różne szkoły, podejścia oraz taktyki. Robert C. Martin w swojej słynnej książce „Clean Code” przedstawia tezę, zgodnie z którą tworząc aplikację od podstaw każda linia kodu produkcyjnego powinna być „pokryta” testem jednostkowym. Z drugiej jednak strony istnieje wiele innych, opisanych w literaturze mniej ortodoksyjnych podejść. W ramach samego tematu testów jednostkowych istnieje również wiele obszarów, które często wywołują rozmaite kontrowersje. Przykładem takiego zagadnienia jest temat testowania niepublicznych elementów aplikakcji. Właśnie temu zagadnieniu poświęcony jest poniższy tekst.

Internet prawdę Ci powie

Na początek przedstawiam Wam odnośniki do dwóch artykułów w których znajdziecie informacje o tym, co na temat testowania metod prywatnych (lub bardziej ogólnie – metod niepublicznych) piszą inni:

Głos „za”

Głos „przeciw”

Dale Emery „Should you unit test private methods on a class?” Chad Myers „Do not test private methods”
Cyt: „do additional investigation”, „loss of cohesion”, „test them indirectly but deliberately” …)

Cyt: „you’re doing something else wrong”, „code stench”, „responsibility violation” …)

Moim zdaniem…

Co na powyższy temat sądzi niżej podpisany? Osobiście preferuje podejście pragmatyczne. Zdaję sobie sprawę, że jeżeli nasz kod zawiera dużą ilość metody prywatnych, które z logicznego punktu widzenia powinny być testowane, to prawdopodobnie coś nie jest w porządku z architekturą naszej aplikacji. W 90% przypadków zostaje w takiej sytuacji naruszona zasada „pojedynczej odpowiedzialności”, a kod który został umieszczony wewnątrz metody prywatnych powinien znajdować się w osobnych klasach. W tym przypadku (teoretycznie) najlepszym podejściem byłby refaktoring, refaktoring i jeszcze raz refaktoring. Z drugiej jednak strony – zdaję sobie sprawę, że zmiana sposobu implementacji za pomocą ww. techniki nie jest zawsze możliwa. Na przeszkodzie mogą stanąć nam następujące kwestie:

  • Kwestie projektowe. Czyli po prostu brak czasu i budżetu lub (hyhyhy…) tzw. „wartości biznesowej”
  • Kwestie polityczne. Dany fragment implementacji nie może być w różnych przyczyn udostępniany jako publiczny.
  • Kwestie historyczne. Cytat wymyślony: „Nasza architektura wygląda tak jak wygląda, sprawdza się od wielu lat i nie będziemy jej zmieniać z byle powodów” (zawał serca).

generimg-phpOpisywana sytuacja będzie z całą pewnością wybitnie niekomfortowa dla większości koderów. Zastana rzeczywistość projektowa (o czym boleśnie przekonuje się wiele osób po studiach) odbiega jednak często od ideału. O tym, czy i kiedy warto toczyć walkę z systemem pisałem już trochę w tym miejscu i generalnie jest to zagadnienie, któremu można poświęcić osobny artykuł. Nie będziemy się więc tym tematem w tym miejscu zajmować.  Do głowy przychodzi mi natomiast jeszcze jedna sytuacja, który może wymusić tworzenie testów jednostkowych prywatnych klas lub komponentów. Jest to źle rozumiane pokrycie kodu (code coverage). Bardzo często, pracując w modelu „klient – dostawca oprogramowania” w umowach możemy natknąć się na wymagania dotyczące pokrycia kodu produkcyjnego testami jednostkowymi. W ekstremalnych sytuacjach wymagane do odbioru systemu liczby mogą dochodzić do chorych wartości w rodzaju 99%. Niektóre ze stosowanych narzędzi sprawdzających ww. parametr biorą pod uwagę również wywoływania metod prywatnych (nie sprawdzając uprzednio czy niepubliczny kod nie jest już testowany w ramach uruchomienia metody publicznej). Na nieszczęście temat ten często wypływa na powierzchnie w momencie odbioru systemu, kiedy na jakiekolwiek negocjacje lub dyskusje o sensowności (lub raczej jej braku) omawianych zapisów jest już za późno.

Co w takim razie powinniśmy czynić w sytuacjach w których musimy mimo wszystko przetestować w sposób bezpośredni niepubliczny fragment kodu. Do głowy przychodzą mi następujące metody:

W przypadku metod wewnętrznych (internal) – użycie atrybutu InternalsVisibleTo

W pliku AssemblyInfo.cs dodajemy następującą deklarację:

[assembly: InternalsVisibleTo("MyTestsLibraryName")]

Gdzie MyTestsLibraryName oznacza nazwę bliblioteki, w której znajdują się nasze testy jednostkowe.

Przykładowo jeżeli za pomocą komponentu MyApplication.MyLibrary.Tests będziemy chcieli przetestować wewnętrzne metody z biblioteki MyApplication.MyLibrary.dll, to wewnątrz pliku AssemblyInfo.cs w drugim z omawianych elementów systemu będziemy musieli dodać następujący atrybut:

[assembly: InternalsVisibleTo("MyApplication.MyLibrary.Tests")]

Uwaga: niektóre biblioteki wykorzystywane w procesie tworzenia testów jednostkowych (np. Moq lub NSubstitute) w przypadku testowania niepublicznych elementów biblioteki wymagają dodania jeszcze jednego atrybutu w pliku AssemblyInfo.cs:

[assembly: InternalsVisibleTo("DynamicProxyGenAssembly2")]

Użycie mechanizmu refleksji (System.Reflection, PrivateObject)

O tym, na czym polega technika refleksji nie będę się w tym miejscu rozpisywał. Osoby, które nie spotkały się wcześniej z tym zagadnieniem polecam szkolenie Introduction to Reflection API. Poniżej znajdziecie natomiast prosty, przykładowy kod w którym za pomocą omawianego mechanizmu testujemy metodę prywatną z klasy Worker.cs za pomocą obiektu PrivateObject.

Klasa testowana:

Klasa testująca:

Podsumowanie

Na pytanie o to, czy w danym przypadku powinniśmy testować niepubliczne komponenty systemu każdy musi sobie odpowiedzieć samodzielnie. Przed podjęciem finalnej decyzji należy rozważyć wszystkie plusy i minusy dostępnych opcji (testy vs refaktoring vs brak testów vs olanie tematu lub zmiana pracy :)). Ja pozwolę sobie tylko wspomnieć, że bezmyślne podążanie za rozmaitymi internetowymi ideologami („testować!”, „nie testować!”, „jeb… system!”) na pewno nie będzie w tym (ani w żadnym innym) przypadku dobrym pomysłem. Pamiętajcie proszę, że nie każdy propagator danej idei jest równocześnie ekspertem lub nawet praktykiem w tym obszarze. Starajmy się rozpatrywać każdy przypadek indywidualnie.

Po prostu (jak mawia bohater ulubionej kreskówki mojej córki):

kubus_cytat17

Total Views: 1031 ,

2 comments

  • Ja osobiście „Jestem za, a nawet przeciw”. Gdy rozpoczynałem przygodę z testowaniem, bawiąc się w wolnych chwilach w TDD, tworząc jakieś mechanizmy bardzo często miałem problem testowania tych mechanizmów, które często były prywatne. Testy niosą za sobą ogromną wartość np. wtedy, gdy kompilacja i uruchomienie odpowiedniego fragmentu aplikacji jest bardzo czasochłonne, a test pozwala nam sprawdzić poprawność pisanego przez nas kodu w mniej niż sekundę. Zapewniają też dużo większą rozwagę przy tworzeniu kodu i oczywiście jego czystość (chociażby przez sam fakt, że kod musi być testowalny). Niestety poza „dłubaniem w domu” nie jest już tak różowo. Największą wadą testów jest to, że trzeba je utrzymywać, a zbyt duże pokrycie testami (dodając do tego np. testy białoskrzynkowe) będzie powodować przy niemal każdej zmianie konieczność zmieniania testów. Moim zdaniem, trzeba znaleźć złoty środek co warto testować, a co jest zbędne jeśli chodzi o przetestowanie i pogodzić się z tym, że programiści robią błędy, które wychwycą później „oby” testerzy, bo nikt za testy nam nie zapłaci, a większość projektów samych w sobie jest już kompromisem między jakością (tutaj bardziej chodzi o ilość funkcji w systemie niż jakoś kodu czy ilość błędów), a budżetem.

Comments are closed.