Testowanie klas niepublicznych

0

Cześć, dzisiaj pytanie o testowanie klas niepublicznych. Załóżmy, że tworzę sobie jakąś bibliotekę. Upubliczniam w niej klasy, które mogą być wykorzystywane w API. Jednak jest część klas, które muszą pozostać prywatne dla biblioteki (klienci API mają nawet o nich nie wiedzieć). Jak testować taką klasę? Gdy uruchamiam xUnit, to oczywiście nie mogę testować, bo klasa nie jest widoczna. I teraz widzę dwie opcje:

  1. Użyć refleksji
  2. Stworzyć nową konfigurację. Do debug i release dodać coś w stylu "TEST". I wtedy:
#ifdef TEST
public
#endif
class InnerClass
{
}

A jak Wy widzicie testowanie takich klas?

5

Ten problem został rozwiązany przez wprowadzenie atrybutu (w .net 2.0, wydawało mi się że później) który sprawia że klasy internal z SUT są widoczne dla projektu testowego

using System.Runtime.CompilerServices;

[assembly:InternalsVisibleTo("MyTests")]

https://stackoverflow.com/questions/358196/c-sharp-internal-access-modifier-when-doing-unit-testing

także nie trzeba kombinować :)

7

InternalsVisibleTo to rak, który się z kodu usuwa, a nie dodaje.

Problem testowania klas wewnętrznych istnieje wyłącznie wtedy, gdy zamiast testów jednostkowych (testujących jednostkę, czyli coś co dostarcza jakąś funkcję w aplikacji) pisze się testy klas (testujących kod klasy). Prawidłowy projekt aplikacji i prawidłowo użyte testy sprawiają, że takich kombinacji nie trzeba.

0

@somekind: w takim razie powiedz mi jak mam przetestować klasę Calculator:

class Calculator
{
   public int Add(int x, int y)
   {
       return x + y;
   }
}

public class MyClass
{
    List<int> numbers;
    public void Add(int x)
    {
        numbers.Add(x);
    }

    public void Operate()
    {
        Calculator calc; //jakoś go pobieram
        //załóżmy, że na liście są dwie liczby
        int sum = calc.Add(numbers[0], numbers[1]);
        
        JakisInnyObiekt obj; //jakoś go pobieram
        obj.DoWork(sum);
    }
}

Powiedz mi jak w mam przetestować metody z klasy Calculator bez używania InternalsVisibleTo. Przecież projekt testowy jest w innym assembly i nie ma dojścia do klasy Calculator.

4

@Juhas: A dlaczego chcesz testować Calculator.Add(...) zamiast MyClass.Operate(...)?

3

Uprzedzając następną odpowiedź: Jeśli Calculator ma skomplikowaną logikę niezależną od MyClass, to powinien znajdować się w oddzielnym module. Wówczas problem z testowaniem też nie istnieje.

0

No zaraz. Testy jednostkowe polegają na testowaniu małych fragmentów. Dlatego chcę testować Add, żeby wiedzieć, że działa. Jeśli przetestowałbym MyClass.Operate i coś by było nie tak, to nie wiem, co jest nie tak. Czy nie działa Calculator, czy może JakisInnyObiekt, a może coś jeszcze innego.

Calculator to klasa przykładowa. Składnik konkretnego modułu. Więc czemu mam ją wywalić do innego modułu?

0

Testy jednostkowe polegają na testowaniu małych fragmentów

Gdzie znalazłeś taką definicję?
Definicja z książki The Art of Unit Testing:

A unit test is a piece of code that invokes a unit of work and checks one specific end result of that unit of work. If the assumptions on the end result turn out to be wrong, the unit test has failed. A unit test’s scope can span as little as a method or as much as multiple classes.

Piszesz jeszcze:

Jeśli przetestowałbym MyClass.Operate i coś by było nie tak, to nie wiem, co jest nie tak. Czy nie działa Calculator, czy może JakisInnyObiekt

Ale jakie to ma znaczenie? Dobry test i tak by Ci to wychwycił. A tak:
a) zakładając, że masz testy tylko do Calculator.Add(...) a nieprawidłowe zachowanie będzie właśnie w JakisInnyObiekt to i tak nie będziesz o tym wiedział.
b) zakładając, że masz testy do Calculator.Add(...) i JakisInnyObiekt - nie zagwarantujesz w żaden sposób, że MyClass.Operate(...) wywołuje je poprawnie.
c) masz testy na Calculator.Add(...), JakisInnyObiekt oraz MyClass.Operate(...) - łamiesz pewnie DRY.

0

Nie mam testów na MyClass.Operate. W zupełności wystarczają mi te dwa pozostałe. Poza tym wszędzie czytam, że jeśli chodzi o pisanie testów, to DRY nie ma znaczenia. Test powinien być jak najprostszy, żeby zminimalizować możliwość wystąpienia błędu w teście. Więc jak to w końcu jest? Bo coraz częściej mam wrażenie, że każdy zupełnie inaczej postrzega testy i każdy ma do nich zupełnie inne podejście.

0

Poza tym wszędzie czytam, że jeśli chodzi o pisanie testów, to DRY nie ma znaczenia.

Oczywiście, każdy przecież lubi po drobnych zmianach w kodzie produkcyjnym poprawiać kod w 30 różnych miejscach w testach. A z TDD to jeszcze lepiej współgra.

0

Staram się w testach trzymać DRY, jednak już w kilku miejscach czytałem, że testy są na tyle specyficzne, że DRY nie ma w nich większego znaczenia.
Nie piszę testów na co dzień. Właściwie to w pracy w ogóle ich nie piszę. Zajmuję się innymi rzeczami. Potrzebuję tego wszystkiego do własnego projektu.

3
Juhas napisał(a):

No zaraz. Testy jednostkowe polegają na testowaniu małych fragmentów. Dlatego chcę testować Add, żeby wiedzieć, że działa. Jeśli przetestowałbym MyClass.Operate i coś by było nie tak, to nie wiem, co jest nie tak. Czy nie działa Calculator, czy może JakisInnyObiekt, a może coś jeszcze innego.

Calculator to klasa przykładowa. Składnik konkretnego modułu. Więc czemu mam ją wywalić do innego modułu?

Są dwie możliwości:

  1. Calculator to odrębny byt, który nie musi być powiązany z MyClass, bo operacja którą wykonuje jest uniwersalna i może być używana także przez inne klasy. (Np. dodawanie, zapis do pliku CSV, mnożenie macierzy). Wtedy taka klasa powinna być wydzielona do innego modułu i przetestowana oddzielnie.
  2. Calculator jest tak ściśle związany z MyClass, i używany tylko przez nią. W tej sytuacji testy MyClass pokryją też Calculator.

Tu była ciekawa dyskusja na temat testowania, mockowania: Testy i mockowanie

Juhas napisał(a):

Nie mam testów na MyClass.Operate. W zupełności wystarczają mi te dwa pozostałe.

No, ale jeśli MyClass jest publiczna, a pozostałe klasy są wewnętrzne, to powinieneś jednak testować właśnie MyClass.

Poza tym wszędzie czytam, że jeśli chodzi o pisanie testów, to DRY nie ma znaczenia. Test powinien być jak najprostszy, żeby zminimalizować możliwość wystąpienia błędu w teście. Więc jak to w końcu jest? Bo coraz częściej mam wrażenie, że każdy zupełnie inaczej postrzega testy i każdy ma do nich zupełnie inne podejście.

No jak to w przypadku każdego zagadnienia jest masa podejść i nieporozumień. Ogólnie są dwie szkoły: londyńska i chicagowska.
Ta pierwsza uczy testowania każdej klasy oddzielnie, mockowania wszystkich jej zależności i sprawdzania czy były wołane.
Ta druga każe skupiać się na tym, czy funkcja zwraca prawidłowy wynik dla określonego wejścia.

Ścisłe trzymanie się pierwszego podejścia sprawia, że w testach masz piekło mocków i masę kodu konfigurującego, ale nie to jest najgorsze. W razie jakiejkolwiek refaktoryzacji, np. przeniesienie kodu do nowej klasy masz masę testów do zmiany. Mimo, że wyniki pozostają takie same!
Dla mnie jest to bardzo niepragmatyczne, testy są po pierwsze po to, aby weryfikowały poprawność kodu, a po drugie po to, aby umożliwiały refaktoryzację. (Jeśli coś zepsujemy w jej trakcie, to testy nie przejdą. Jeśli przechodzą, to niczego nie zepsuliśmy.) Testy, w których testowana jest każda klasa samodzielnie, utrudniają refaktoryzację, po zmianie kodu biznesowego nie wiadomo czy go przypadkiem zepsuliśmy, czy to testy są teraz niepoprawne. W efekcie możemy naprawić nie to co trzeba i zostawić kod produkcyjny zepsuty.

mstl napisał(a):

Poza tym wszędzie czytam, że jeśli chodzi o pisanie testów, to DRY nie ma znaczenia.

Oczywiście, każdy przecież lubi po drobnych zmianach w kodzie produkcyjnym poprawiać kod w 30 różnych miejscach w testach. A z TDD to jeszcze lepiej współgra.

O ile dobrze rozumiem, to chodzi o DRY kodu testów, a nie logiki. A to już nie jest takie proste... bo o ile DRY jest oczywiście ważne, to jednak przesadne dążenie do DRY w kodzie testów może mieć dziwne konsekwencje. Np. kiedyś pisałem testy do buildera, więc potrzebowałem oczywiście zestawu spodziewanych obiektów do różnych przypadków testowych, zazwyczaj różniących się jedną-dwiema wartościami. Aby uniknąć kopiuj-wklej napisałem do nich kolejny builder... na szczęście w porę się opamiętałem. ;)

0

OK, widzę że zagadnienie nie jest tak oczywiste, jak myślałem. Więc podam konkretny przykład. Mam klasę Serializer. No to tak naprawdę jest jakaś abstrakcyjna klasa i po niej dziedziczą konkretne serializery (do pliku binarnego, do xml itd). I mam też klasę dokument. To jest klasa publiczna. Wykorzystuje konkretny serializer. Nie mam możliwości przetestować jej odpowiednio, bo rezultatem metody Save w klasie Document jest plik na dysku. Dlatego zdecydowałem się na testy konkretnych serializerów. Serializery są faktycznie wewnętrzną klasą, używaną przez Document, ale klienci nie powinni wiedzieć o jej istnieniu. Mają posługiwać się klasą Document, a nie Serializerem. Poza tym obie klasy siedzą w jednym PCL'owym assembly. Niedługo zacznę dodawać nowe projekty - Xamarin, WinFormsy i co jeszcze tam będzie trzeba. Dlatego też opcja z InternalsVisibleTo wydaje mi się najlepsza w tym przypadku. No bo nie widzę sensu w tworzeniu nowego assembly tylko po to, żeby trzymało Serializera. Serializery są w osobnym namespace i wg mnie to jest ok.

0

Myślę sobie że ta dyskusja do niczego nie prowadzi. Cały ten przykład( Calculator) już na samym początku ma złą architekturę i jest źle napisany, więc co tu testować

0

Ja zazwyczaj wszystkie tego typu pomocnicze byty trzymam w module o nazwie Infrastructure, tam są publiczne i używane przez konkretne klasy z modułów biznesowych.

0

Zrob interface do klasy Serializer a nastepnie stworz jego mocka (poczytaj) - wtedy bedziesz mogl testowac klase Document (i to ja masz testowac). Dodatkowo kazdy serializer (jesli jest pisany przez Ciebie) tez powinienes testowac - ewentulnie testy integracyjne dla klasy Document z konkretnymi implementacjami.

0
somekind napisał(a):

InternalsVisibleTo to rak, który się z kodu usuwa, a nie dodaje.

To zależy. Załóżmy, że buduję moduł do komunikacji z serwisem zewnętrznym. Jedyne co chcę udostępnić klientowi to metody które odpowiedzialne za przyjęcie żądania i klasy które te metody przyjmują oraz zwracają w odpowiedzi. Cały ten moduł może mieć skomplikowaną logikę pod spodem, która powinna być testowana, natomiast nie ma sensu udostępniać jej innym, które z niego korzystają.

2

Podzielenie modułu na warstwy, w których część metod jest publiczna nie oznacza od razu upublicznienia. A to, że coś jest publiczne nie znaczy od razu, że trzeba tego używać.

A moduł, którego celem jest komunikacja z serwisem zewnętrznym i tak nie da się przetestować inaczej niż integracyjnie.

0

Za to da się przetestować jednostkowo chociażby parsery odpowiedzi

0

Owszem, można. Ale to jest jakiś argument za robieniem ich internal? Bo jeśli tak, to schodzimy na temat pisania aplikacji jednowarstwowych, a na tym się nie znam.

0

Test jednostkowy testuje jednostkę czyli jedną funkcjonalność w systemie a nie "kawałek kodu".
Nadmierne stosowanie DRY prowadzi do dodatkowych zależności co za tym idzie do pogorszenia czytelności testów.

0

W tym przypadku, dużo korzystniejsze jest przetestowanie tylko klasy MyClass, nie ma tutaj wątpliwości. Klasa Calculator zawiera proste operacje - tutaj testowanie bym pominął.
Uważam też, że testowanie internalowych klas ma swoje korzyści i nie upubliczniał bym niektórej logiki, aby tylko pozbyć się InternalsVisibleTo.


Czasem warto jest przetestować wewnętrzne obiekty klasy, która jest jakąś fasadą i skupia w sobie wiele małych obiektów, którymi niekoniecznie chcemy się chwalić.

Tą klasę - fasadę obkładamy tylko wtedy jakimś testem integracyjnym lub kilkoma testami, które nie testują wszystkich możliwych ścieżek.
Testowanie takiego złożonego obiektu bardzo dokładnie nie ma sensu ponieważ na jego poziomie mamy eksplozję kombinatoryczną przypadków.

Suma testów jeśli testujemy integracyjnie to [Ilość testów klasy A] * [ilość testów klasy B] * ... * [ilość testów klasy N]

Warto zapamiętać, że nie ma jednej, jedynej drogi, takiego złotego środka :)
Nie trzeba również mieć 100% pokrycia ;)

1
Łukasz Hylicki napisał(a):

W tym przypadku, dużo korzystniejsze jest przetestowanie tylko klasy MyClass, nie ma tutaj wątpliwości. Klasa Calculator zawiera proste operacje - tutaj testowanie bym pominął.

Przecież to był tylko przykład, nie o testowanie dodawania w tym problemie chodzi.


Też kiedyś uważałem, że warto testować klasy i metody prywatne, nawet się o to kłóciłem jakieś siedem lat temu stosując podobną argumentację jak koledzy wyżej.
No, ale z tego wyrosłem. Na przestrzeni ostatnich lat zawsze gdy miałem coś internalowego wartego przetestowania, po chwili namysłu okazywało się, że powinno się znaleźć w oddzielnym module. A gdy zaczyna się sensownie strukturyzować kod, magia staje się zbędna.

0
somekind napisał(a):

Przecież to był tylko przykład, nie o testowanie dodawania w tym problemie chodzi.

Nie no jasne, że tylko przykład. Rozumiem.

somekind napisał(a):

Też kiedyś uważałem, że warto testować klasy i metody prywatne, nawet się o to kłóciłem jakieś siedem lat temu stosując podobną argumentację jak koledzy wyżej.
No, ale z tego wyrosłem. Na przestrzeni ostatnich lat zawsze gdy miałem coś internalowego wartego przetestowania, po chwili namysłu okazywało się, że powinno się znaleźć w oddzielnym module. A gdy zaczyna się sensownie strukturyzować kod, magia staje się zbędna.

Tutaj się nie zgodzę jednak, bo to zależy od klasy problemu. Jeśli chcesz ukryć głęboką fizykę swojego rozwiązania, ale jednocześnie nie chcesz testować tylko integracyjnie z uwagi na ilość ścieżek, które należy sprawdzić, to pokusisz się o przetestowanie swoich internalowych komponentów. Mając 3 komponenty wewnątrz przecież lepiej jest napisać 10+10+10=30 testów, niż 10*10*10=1000 testów, czyż nie?

Myślę, że to kwestia realnej oceny sytuacji :)

3
Łukasz Hylicki napisał(a):

Jeśli chcesz ukryć głęboką fizykę swojego rozwiązania, ale jednocześnie nie chcesz testować tylko integracyjnie z uwagi na ilość ścieżek, które należy sprawdzić, to pokusisz się o przetestowanie swoich internalowych komponentów.

Nie. Wydzielę je do modułu fizyki, z którego wystawię publiczne API (w sensie zestawu klas do wywołania w oddzielnej DLLce, nie serwisu webowego dostępnego dla całego świata), które przetestuję jednostkowo.

0
somekind napisał(a):

Nie. Wydzielę je do modułu fizyki, z którego wystawię publiczne API, które przetestuję jednostkowo.

Wedle uznania, lecz stosowanie jednego podejścia do każdego problemu nie zawsze się sprawdza :)

Jednak w wielu produktach się stosuje podejście z InternalsVisibleTo. Ostatnio korzystam z Nancy i tam jest tak samo właśnie.

0
Łukasz Hylicki napisał(a):

Uważam też, że testowanie internalowych klas ma swoje korzyści i nie upubliczniał bym niektórej logiki, aby tylko pozbyć się InternalsVisibleTo.

:)

A ktoś słyszał o czymś takim jak framework izolacji( Moćkowania :D ) który wykorzystuje API Profiler ?
Jeśli chcesz sprawdzić wywołanie prywatnej metody to dało by radę przez np. TypeMock.
Co do testowania To nie można użyć mechanizmu refleksji.... ?

0
MrBean Bean napisał(a):

A ktoś słyszał o czymś takim jak framework izolacji( Moćkowania :D ) który wykorzystuje API Profiler ?
Jeśli chcesz sprawdzić wywołanie prywatnej metody to dało by radę przez np. TypeMock.
Co do testowania To nie można użyć mechanizmu refleksji.... ?

Nie testuje się prywatnych metod w ogóle. Jeżeli masz kod, do którego chcesz wprowadzić testy, a kod nie jest testowalny, to polecam upublicznić metodę, którą chcesz przetestować. Przez jakiś czas naruszysz hermetyzację, ale to koszt wprowadzania testów do kodu legacy.
Częściowe, tymczasowe popsucie API to nic złego jeśli strategicznie zyskujesz więcej niż tracisz :)

0
Łukasz Hylicki napisał(a):

Nie testuje się prywatnych metod w ogóle.

W przypadku części moich programów takie podejście byłoby absurdem.

W prywatnych metodach mam np. implementacje bardzo skomplikowanych algorytmów statystycznych, gdzie można łatwo gdzieś się pomylić, a publiczne np. przyjmują informacje o dwóch produktach i używając tych skomplikowanych prywatnych określają czy zakup pierwszego jest bardziej opłacalny niż drugiego.

0
Łukasz Hylicki napisał(a):
MrBean Bean napisał(a):

A ktoś słyszał o czymś takim jak framework izolacji( Moćkowania :D ) który wykorzystuje API Profiler ?
Jeśli chcesz sprawdzić wywołanie prywatnej metody to dało by radę przez np. TypeMock.
Co do testowania To nie można użyć mechanizmu refleksji.... ?

Nie testuje się prywatnych metod w ogóle. Jeżeli masz kod, do którego chcesz wprowadzić testy, a kod nie jest testowalny, to polecam upublicznić metodę, którą chcesz przetestować. Przez jakiś czas naruszysz hermetyzację, ale to koszt wprowadzania testów do kodu legacy.
Częściowe, tymczasowe popsucie API to nic złego jeśli strategicznie zyskujesz więcej niż tracisz :)

Czyli najpierw psujesz kod jeszcze bardziej a potem go testujesz ? Przecież to absurd, jeśli z jakiegoś powodu potrzebujesz w testach operować na prywatnych metodach to jak mówiłem API Profiler i mechanizm refleksji. No chyba że Chcesz umieszczać w kodzie produkcyjnym jakieś dziwne twory jak internalvisiblefor

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