Czy można sfałszować część testowanej klasy?

22

Załóżmy, że mam klasę (wybacz wymyślony przykład i jego zły projekt):

class MyProfit
{
  public decimal GetNewYorkRevenue();
  public decimal GetNewYorkExpenses();
  public decimal GetNewYorkProfit();

  public decimal GetMiamiRevenue();
  public decimal GetMiamiExpenses();
  public decimal GetMiamiProfit();

  public bool BothCitiesProfitable();

}

(Uwaga: metody GetxxxRevenue () i GetxxxExpenses () mają zależności, które są eliminowane)

Teraz przeprowadzam testy jednostkowe BothCitiesProfitable (), który zależy od GetNewYorkProfit () i GetMiamiProfit (). Czy można pobierać kody GetNewYorkProfit () i GetMiamiProfit ()?

Wygląda na to, że jeśli tego nie zrobię, jednocześnie jednocześnie testuję GetNewYorkProfit () i GetMiamiProfit () wraz z BothCitiesProfitable (). Muszę się upewnić, że skonfigurowałem kodowanie dla GetxxxRevenue () i GetxxxExpenses (), aby metody GetxxxProfit () zwróciły prawidłowe wartości.

Do tej pory widziałem tylko przykład polegający na pobieraniu zależności od klas zewnętrznych, a nie wewnętrznych metod.

A jeśli jest w porządku, czy jest jakiś konkretny wzór, którego powinienem użyć, aby to zrobić?

AKTUALIZACJA

Obawiam się, że brakuje nam podstawowego problemu i to prawdopodobnie wina mojego złego przykładu. Podstawowe pytanie brzmi: jeśli metoda w klasie jest zależna od innej publicznie ujawnionej metody w tej samej klasie, czy jest w porządku (a nawet zalecane) usunięcie tej innej metody?

Może czegoś mi brakuje, ale nie jestem pewien, czy podział klasy zawsze ma sens. Być może innym minimalnie lepszym przykładem byłoby:

class Person
{
 public string FirstName()
 public string LastName()
 public string FullName()
}

gdzie pełna nazwa jest zdefiniowana jako:

public string FullName()
{
  return FirstName() + " " + LastName();
}

Czy można testować FirstName () i LastName () podczas testowania FullName ()?

Użytkownik
źródło
Jeśli testujesz jakiś kod jednocześnie, jak może być źle? Jaki jest problem? Zwykle lepiej i częściej testować kod.
użytkownik nieznany
Właśnie dowiedziałem się o twojej aktualizacji, zaktualizowałem swoją odpowiedź
Winston Ewert

Odpowiedzi:

27

Powinieneś rozbić klasę, o której mowa.

Każda klasa powinna wykonać proste zadanie. Jeśli zadanie jest zbyt skomplikowane do przetestowania, zadanie, które wykonuje klasa, jest zbyt duże.

Ignorując głupotę tego projektu:

class NewYork
{
    decimal GetRevenue();
    decimal GetExpenses();
    decimal GetProfit();
}


class Miami
{
    decimal GetRevenue();
    decimal GetExpenses();
    decimal GetProfit();
}

class MyProfit
{
     MyProfit(NewYork new_york, Miami miami);
     boolean bothProfitable();
}

AKTUALIZACJA

Problem z metodami stubowania w klasie polega na tym, że naruszasz enkapsulację. Twój test powinien sprawdzić, czy zewnętrzne zachowanie obiektu odpowiada specyfikacjom. Cokolwiek dzieje się wewnątrz obiektu, nie jest jego sprawą.

Fakt, że FullName używa FirstName i LastName jest szczegółem implementacji. Nic poza klasą nie powinno dbać o to, że to prawda. Wyśmiewając publiczne metody w celu przetestowania obiektu, przyjmujesz założenie, że ten obiekt został zaimplementowany.

W pewnym momencie w przyszłości założenie to może przestać być poprawne. Być może cała logika nazwy zostanie przeniesiona do obiektu Name, który Osoba po prostu wywołuje. Być może FullName będzie bezpośrednio uzyskiwać dostęp do zmiennych członków first_name i last_name zamiast wywoływać FirstName i LastName.

Drugie pytanie dotyczy tego, dlaczego czujesz taką potrzebę. W końcu Twoja klasa osobowa może zostać przetestowana w następujący sposób:

Person person = new Person("John", "Doe");
Test.AssertEquals(person.FullName(), "John Doe");

W tym przykładzie nie powinieneś odczuwać potrzeby usuwania niczego. Jeśli to zrobisz, jesteś bardzo szczęśliwy i dobrze ... przestań! Wyśmiewanie metod nie ma żadnej korzyści, ponieważ i tak masz kontrolę nad tym, co w nich jest.

Jedynym przypadkiem, w którym wydaje się sensowne, aby metody stosowane przez FullName były wyśmiewane, jest to, że FirstName () i LastName () były operacjami nietrywialnymi. Być może piszesz jeden z tych generatorów losowych nazw lub Imię i Nazwisko wyszukuje w bazie danych odpowiedź lub coś takiego. Ale jeśli tak się dzieje, sugeruje, że obiekt robi coś, co nie należy do klasy Person.

Innymi słowy, naśmiewanie się z metod polega na zabraniu obiektu i rozbiciu go na dwie części. Jeden kawałek jest wyśmiewany, podczas gdy drugi jest testowany. To, co robisz, to w zasadzie doraźne rozbicie obiektu. W takim przypadku wystarczy rozbić obiekt.

Jeśli twoja klasa jest prosta, nie powinieneś odczuwać potrzeby kpienia z niej podczas testu. Jeśli twoja klasa jest na tyle złożona, że ​​czujesz potrzebę kpienia, powinieneś podzielić ją na prostsze części.

PONOWNIE AKTUALIZACJA

Z mojego punktu widzenia obiekt ma zachowanie zewnętrzne i wewnętrzne. Zachowanie zewnętrzne obejmuje zwracanie wartości wywołań do innych obiektów itp. Oczywiście wszystko w tej kategorii powinno zostać przetestowane. (inaczej co byś przetestował?) Ale zachowanie wewnętrzne nie powinno być tak naprawdę testowane.

Teraz testowane jest zachowanie wewnętrzne, ponieważ właśnie to powoduje zachowanie zewnętrzne. Ale nie piszę testów bezpośrednio na temat zachowania wewnętrznego, tylko pośrednio poprzez zachowanie zewnętrzne.

Jeśli chcę coś przetestować, uważam, że należy go przenieść, aby stał się zachowaniem zewnętrznym. Dlatego myślę, że jeśli chcesz coś wyśmiewać, powinieneś podzielić obiekt tak, aby rzecz, którą chcesz wyśmiewać, teraz zachowuje się na zewnątrz obiektów, o których mowa.

Ale jaka to różnica? Jeśli FirstName () i LastName () są członkami innego obiektu, czy to naprawdę zmienia problem FullName ()? Czy jeśli zdecydujemy, że konieczne jest kpiny z FirstName, a LastName naprawdę pomaga im znajdować się na innym obiekcie?

Myślę, że jeśli zastosujesz swoje kpiące podejście, wtedy utworzysz szew w obiekcie. Masz funkcje takie jak FirstName () i LastName (), które bezpośrednio komunikują się z zewnętrznym źródłem danych. Masz również FullName (), która nie ma. Ale ponieważ wszyscy należą do tej samej klasy, nie jest to oczywiste. Niektóre elementy nie powinny mieć bezpośredniego dostępu do źródła danych, a inne są. Twój kod będzie wyraźniejszy, jeśli tylko rozbijesz te dwie grupy.

EDYTOWAĆ

Cofnijmy się i zapytajmy: dlaczego kpimy z obiektów podczas testowania?

  1. Spraw, aby testy działały spójnie (unikaj dostępu do rzeczy, które zmieniają się z uruchomienia na uruchomienie)
  2. Unikaj dostępu do drogich zasobów (nie korzystaj z usług stron trzecich itp.)
  3. Uprość testowany system
  4. Ułatw testowanie wszystkich możliwych scenariuszy (np. Symulowanie awarii itp.)
  5. Unikaj polegania na szczegółach innych fragmentów kodu, aby zmiany w tych innych fragmentach kodu nie przerwały tego testu.

Myślę, że przyczyny 1-4 nie dotyczą tego scenariusza. Wyśmiewanie zewnętrznego źródła podczas testowania pełnej nazwy rozwiązuje wszystkie te powody wyśmiewania. Jedyną nieobsługiwaną częścią jest prostota, ale wydaje się, że obiekt jest wystarczająco prosty, co nie stanowi problemu.

Myślę, że twoja obawa jest powodem numer 5. Obawa polega na tym, że w pewnym momencie zmiana implementacji FirstName i LastName przerwie test. W przyszłości FirstName i LastName mogą uzyskać nazwy z innej lokalizacji lub źródła. Ale FullName prawdopodobnie zawsze będzie FirstName() + " " + LastName(). Dlatego chcesz przetestować FullName, wyśmiewając FirstName i LastName.

To, co masz, to jakiś podzbiór obiektu osoby, który z większym prawdopodobieństwem zmieni się niż inne. Reszta obiektu używa tego podzbioru. Ten podzbiór obecnie pobiera dane przy użyciu jednego źródła, ale może później pobrać te dane w zupełnie inny sposób. Ale dla mnie wygląda na to, że ten podzbiór jest odrębnym przedmiotem próbującym się wydostać.

Wydaje mi się, że jeśli wyśmiewasz metodę obiektu, dzielisz obiekt. Ale robisz to w sposób doraźny. Twój kod nie wyjaśnia, że ​​wewnątrz obiektu Person znajdują się dwa różne elementy. Po prostu podziel ten obiekt na rzeczywisty kod, aby było jasne odczytywanie kodu, co się dzieje. Wybierz rzeczywisty podział obiektu, który ma sens, i nie próbuj dzielić obiektu w inny sposób dla każdego testu.

Podejrzewam, że możesz sprzeciwić się podziałowi swojego przedmiotu, ale dlaczego?

EDYTOWAĆ

Myliłem się.

Powinieneś dzielić obiekty, a nie wprowadzać podziały ad-hoc, wyśmiewając poszczególne metody. Jednak nadmiernie skupiłem się na jednej metodzie podziału obiektów. Jednak OO zapewnia wiele metod podziału obiektu.

Co bym zaproponował:

class PersonBase
{
      abstract sring FirstName();
      abstract string LastName();

      string FullName()
      {
            return FirstName() + " " + LastName();
      }
 }

 class Person extends PersonBase
 {
      string FirstName(); 
      string LastName();
 }

 class FakePerson extends PersonBase
 {
      void setFirstName(string);
      void setLastName(string);
      string getFirstName();
      string getLastName();
 }

Może to właśnie robiłeś przez cały czas. Ale nie sądzę, aby ta metoda miała problemy, które widziałem w przypadku metod kpienia, ponieważ wyraźnie nakreśliliśmy, po której stronie jest każda metoda. Używając dziedziczenia, unikamy niezręczności, która pojawiłaby się, gdybyśmy użyli dodatkowego obiektu opakowania.

Wprowadza to pewną złożoność i dla tylko kilku funkcji użyteczności prawdopodobnie po prostu przetestuję je, kpiąc z podstawowego źródła zewnętrznego. Oczywiście są narażeni na większe ryzyko pęknięcia, ale nie warto tego zmieniać. Jeśli masz wystarczająco skomplikowany obiekt, który musisz podzielić, to myślę, że coś takiego jest dobrym pomysłem.

Winston Ewert
źródło
8
Dobrze powiedziane. Jeśli próbujesz znaleźć obejścia w testowaniu, prawdopodobnie musisz ponownie rozważyć swój projekt (na marginesie, teraz mam głos Franka Sinatry utkwił mi w głowie).
Problematyczny
2
„Kpiąc z publicznych metod w celu przetestowania obiektu, przyjmujesz założenie, że [jak] ten obiekt jest implementowany”. Ale czy nie jest tak w przypadku zarzucenia obiektu? Załóżmy na przykład, że znam moją metodę xyz () wywołuje inny obiekt. Aby przetestować xyz () w izolacji, muszę usunąć drugi obiekt. Dlatego mój test musi wiedzieć o szczegółach implementacji mojej metody xyz ().
Użytkownik
W moim przypadku metody „FirstName ()” i „LastName ()” są proste, ale sprawdzają wyniki interfejsu API innej firmy.
Użytkownik
@ Użytkownik, zaktualizowano, prawdopodobnie kpisz z zewnętrznego interfejsu API, aby przetestować FirstName i LastName. Co jest złego w robieniu tego samego drwiny podczas testowania FullName?
Winston Ewert
@Winston, to w pewnym sensie mój punkt, obecnie kpię z zewnętrznego interfejsu API używanego w imię i nazwisko, aby przetestować imię i nazwisko (), ale wolałbym nie dbać o to, jak imię i nazwisko są implementowane podczas testowania imienia i nazwiska (oczywiście jest ok, kiedy testuję imię i nazwisko). Stąd moje pytanie o szydercze imię i nazwisko.
Użytkownik
10

Chociaż zgadzam się z odpowiedzią Winstona Ewerta, czasem po prostu nie jest możliwe rozbicie klasy, czy to z powodu ograniczeń czasowych, API już używanego gdzie indziej, czy co masz.

W takim przypadku zamiast szydzić z metod, pisałbym moje testy, by stopniowo pokrywać klasę. Przetestuj metody getExpenses()i getRevenue(), a następnie przetestuj getProfit()metody, a następnie przetestuj metody udostępnione. To prawda, że ​​będziesz mieć więcej niż jeden test obejmujący określoną metodę, ale ponieważ napisałeś testy zaliczające poszczególne metody, możesz mieć pewność, że wyniki są wiarygodne, biorąc pod uwagę przetestowane dane wejściowe.

Problematyczny
źródło
1
Zgoda. Ale tylko, aby podkreślić punkt dla każdego, kto to czyta, jest to rozwiązanie, którego używasz, jeśli nie możesz zrobić lepszego rozwiązania.
Winston Ewert
5
@Winston, i to jest rozwiązanie, którego używasz przed przejściem do lepszego rozwiązania. Tzn. Mając dużą kulę starszego kodu, musisz objąć go testami jednostkowymi, zanim będzie można go ponownie wprowadzić.
Péter Török,
@ Péter Török, dobry punkt. Chociaż myślę, że jest to uwzględnione w punkcie „Nie mogę zrobić lepszego rozwiązania”. :)
Winston Ewert
@all Testowanie metody getProfit () będzie bardzo trudne, ponieważ różne scenariusze dla getExpenses () i getRevenues () faktycznie pomnożą scenariusze potrzebne dla getProfit (). jeśli testujemy getExpenses () i get Revains () niezależnie Czy nie jest dobrym pomysłem wyśmiewanie tych metod testowania getProfit ()?
Mohd Farid
7

Aby jeszcze bardziej uprościć swoje przykłady, powiedz, że testujesz C(), co zależy od A()i B()każdy z nich ma swoje zależności. IMO sprowadza się do tego, co twój test próbuje osiągnąć.

Jeśli testujesz zachowanie C()podanych znanych zachowań A()i B()to chyba najprostszy i najlepszy do skrótową się A()i B(). Prawdopodobnie purystowie nazywają to testem jednostkowym.

Jeśli testujesz zachowanie całego systemu (z C()perspektywy), opuściłbym A()i B()jako zaimplementowałbym albo wytłumaczył ich zależności (jeśli umożliwia testowanie) lub skonfigurował środowisko sandbox, takie jak test Baza danych. Nazwałbym to testem integracyjnym.

Oba podejścia mogą być poprawne, więc jeśli jest zaprojektowany tak, aby można go było przetestować z góry, prawdopodobnie na dłuższą metę będzie lepiej.

Rebecca Scott
źródło