Jak testować jednostkowo klasy abstrakcyjne: rozszerzaj za pomocą kodów pośredniczących?

446

Zastanawiałem się, jak testować jednostkowo klasy abstrakcyjne i klasy rozszerzające klasy abstrakcyjne.

Czy powinienem przetestować klasę abstrakcyjną, rozszerzając ją, usuwając metody abstrakcyjne, a następnie testując wszystkie konkretne metody? Potem testujesz tylko metody, które zastępuję i testujesz metody abstrakcyjne w testach jednostkowych dla obiektów, które rozszerzają moją klasę abstrakcyjną?

Czy powinienem mieć abstrakcyjny przypadek testowy, którego można użyć do testowania metod klasy abstrakcyjnej, i rozszerzyć tę klasę w moim przypadku testowym o obiekty, które rozszerzają klasę abstrakcyjną?

Zauważ, że moja klasa abstrakcyjna ma pewne konkretne metody.

Paul Whelan
źródło

Odpowiedzi:

268

Napisz próbny obiekt i użyj go tylko do testowania. Zazwyczaj są one bardzo bardzo bardzo minimalne (dziedziczą po klasie abstrakcyjnej) i nie więcej. Następnie w teście jednostkowym możesz wywołać metodę abstrakcyjną, którą chcesz przetestować.

Powinieneś przetestować klasy abstrakcyjne, które zawierają logikę, podobnie jak wszystkie inne klasy, które masz.

Patrick Desjardins
źródło
9
Cholera, muszę powiedzieć, że po raz pierwszy zgodziłem się na pomysł użycia kpiny.
Jonathan Allen,
5
Potrzebujesz dwóch klas, kpiny i testu. Klasa próbna rozszerza jedynie abstrakcyjne metody testowanej klasy Abstract. Metody te mogą być no-op, zwracać null itp., Ponieważ nie będą testowane. Klasa testowa testuje tylko nieabstrakcyjny publiczny interfejs API (tj. Interfejs zaimplementowany przez klasę Abstract). Dla każdej klasy rozszerzającej klasę Abstract potrzebne będą dodatkowe klasy testowe, ponieważ metody abstrakcyjne nie zostały uwzględnione.
cyber-mnich
10
Oczywiście można to zrobić ... ale aby naprawdę przetestować dowolną klasę pochodną, ​​będziesz testować tę podstawową funkcjonalność w kółko .. co prowadzi cię do posiadania abstrakcyjnego urządzenia testowego, abyś mógł uwzględnić tę duplikację podczas testowania. To wszystko pachnie! Zdecydowanie polecam przyjrzeć się, dlaczego w pierwszej kolejności używasz klas abstrakcyjnych i zobaczyć, czy coś innego działałoby lepiej.
Nigel Thorne
5
przesadna kolejna odpowiedź jest znacznie lepsza.
Martin Spamer
22
@MartiSpamer: Nie powiedziałbym, że ta odpowiedź jest zawyżona, ponieważ została napisana znacznie wcześniej (2 lata) niż odpowiedź, którą uważasz za lepszą poniżej. Zachęcamy tylko Patryka, ponieważ w kontekście, kiedy opublikował tę odpowiedź, było świetnie. Zachęcajmy się nawzajem. Na zdrowie
Marvin Thobejane,
449

Istnieją dwa sposoby wykorzystania abstrakcyjnych klas bazowych.

  1. Specjalizujesz się w swoim abstrakcyjnym obiekcie, ale wszyscy klienci będą korzystać z pochodnej klasy poprzez interfejs podstawowy.

  2. Używasz abstrakcyjnej klasy bazowej, aby wykluczyć duplikację obiektów w twoim projekcie, a klienci używają konkretnych implementacji poprzez swoje własne interfejsy.!


Rozwiązanie dla 1 - wzorzec strategii

Opcja 1

Jeśli masz pierwszą sytuację, to faktycznie masz interfejs zdefiniowany przez metody wirtualne w klasie abstrakcyjnej, którą implementują twoje klasy pochodne.

Powinieneś rozważyć uczynienie tego prawdziwym interfejsem, zmianę swojej klasy abstrakcyjnej na konkretną i wzięcie instancji tego interfejsu w jego konstruktorze. Twoje pochodne klasy stają się implementacjami tego nowego interfejsu.

IMotor

Oznacza to, że możesz teraz przetestować swoją poprzednio abstrakcyjną klasę za pomocą fałszywej instancji nowego interfejsu i każdej nowej implementacji za pośrednictwem teraz publicznego interfejsu. Wszystko jest proste i możliwe do przetestowania.


Rozwiązanie dla 2

Jeśli masz drugą sytuację, twoja klasa abstrakcyjna działa jako klasa pomocnicza.

AbstractHelper

Spójrz na funkcje, które zawiera. Sprawdź, czy którykolwiek z nich można zepchnąć na obiekty, którymi manipulujesz, aby zminimalizować to powielanie. Jeśli nadal masz coś do zrobienia, spójrz na uczynienie z niego klasy pomocniczej, którą Twoja konkretna implementacja przyjmuje w swoim konstruktorze i usuń ich klasę podstawową.

Motor Helper

To znowu prowadzi do konkretnych klas, które są proste i łatwe do przetestowania.


Z zasady

Preferuj złożoną sieć prostych obiektów nad prostą siecią złożonych obiektów.

Kluczem do rozszerzalnego kodu testowalnego są małe bloki konstrukcyjne i niezależne okablowanie.


Zaktualizowano: Jak obsługiwać mieszanki obu?

Możliwe jest, aby klasa podstawowa pełniła obie te role ... tzn .: ma interfejs publiczny i chroni metody pomocnicze. W takim przypadku można rozdzielić metody pomocnicze na jedną klasę (scenariusz 2) i przekonwertować drzewo dziedziczenia na wzorzec strategii.

Jeśli okaże się, że masz metody, które bezpośrednio implementuje klasa podstawowa, a inne są wirtualne, nadal możesz przekonwertować drzewo dziedziczenia na wzorzec strategii, ale uznałbym to również za dobry wskaźnik, że obowiązki nie są odpowiednio wyrównane i może potrzebujesz refaktoryzacji.


Aktualizacja 2: Klasy abstrakcyjne jako odskocznia (2014/06/12)

Pewnego dnia miałem sytuację, w której użyłem abstrakcji, więc chciałbym dowiedzieć się, dlaczego.

Mamy standardowy format dla naszych plików konfiguracyjnych. To narzędzie ma 3 pliki konfiguracyjne w tym formacie. Chciałem silnie wpisanej klasy dla każdego pliku ustawień, więc poprzez wstrzyknięcie zależności klasa może poprosić o ustawienia, na których mu zależy.

Zaimplementowałem to, mając abstrakcyjną klasę bazową, która wie, jak analizować formaty plików ustawień i klasy pochodne, które ujawniały te same metody, ale zawierały lokalizację pliku ustawień.

Mógłbym napisać „SettingsFileParser”, który 3 klasy owinęły, a następnie przekazały do ​​klasy podstawowej, aby ujawnić metody dostępu do danych. Nie zdecydowałem się tego jeszcze zrobić, ponieważ doprowadziłoby to do powstania 3 klas pochodnych z większą ilością kodu delegacji niż cokolwiek innego.

Jednak ... wraz z ewolucją tego kodu, a odbiorcy każdej z tych klas ustawień stają się coraz wyraźniejsi. Każdy z ustawień poprosi o pewne ustawienia i przekształci je w jakiś sposób (ponieważ ustawienia są tekstem, mogą zawinąć je w obiekty i przekonwertować na liczby itp.). Gdy to się stanie, zacznę wyodrębniać tę logikę do metod manipulacji danymi i wypychać je z powrotem do silnie wpisanych klas ustawień. Doprowadzi to do interfejsu wyższego poziomu dla każdego zestawu ustawień, który ostatecznie nie będzie już świadomy, że ma do czynienia z „ustawieniami”.

W tym momencie mocno wpisane klasy ustawień nie będą już potrzebowały metod „getter”, które ujawniają bazową implementację „ustawień”.

W tym momencie nie chciałbym już, aby ich interfejs publiczny zawierał metody akcesora ustawień; więc zmienię tę klasę, aby hermetyzowała klasę parsera ustawień zamiast z niej czerpać.

Klasa Abstract jest zatem: sposobem na uniknięcie w tej chwili kodu delegacji oraz znacznikiem w kodzie, który przypomina mi o zmianie projektu później. Być może nigdy się do tego nie dostanę, więc może żyć długo ... tylko kod może to stwierdzić.

Uważam, że jest to prawdą w przypadku każdej reguły ... jak „brak metod statycznych” lub „brak metod prywatnych”. Wskazują zapach w kodzie ... i to dobrze. Pomaga ci szukać abstrakcji, którą przegapiłeś ... i pozwala w międzyczasie nadal dostarczać wartość klientowi.

Wyobrażam sobie takie reguły, które definiują krajobraz, w którym w dolinach żyje możliwy do utrzymania kod. Gdy dodajesz nowe zachowanie, to jest jak deszcz lądujący na twoim kodzie. Początkowo umieszczasz go gdziekolwiek wyląduje .. następnie refaktoryzujesz, aby siły dobrego projektu popchnęły zachowanie, aż wszystko skończy się w dolinach.

Nigel Thorne
źródło
18
To świetna odpowiedź. Znacznie lepiej niż najwyżej oceniane. Ale sądzę, że tylko ci, którzy naprawdę chcą napisać testowalny kod, doceniliby to .. :)
MalcomTucker
22
Nie mogę się pogodzić z tym, jak naprawdę jest to dobra odpowiedź. To całkowicie zmieniło mój sposób myślenia o klasach abstrakcyjnych. Dzięki Nigel.
MalcomTucker
4
O nie ... kolejna zasada, którą muszę przemyśleć! Dzięki (zarówno sarkastycznie, jak i non-sarkastycznie, po tym jak zasymilowałem to i czuję się lepszym programistą)
Martin Lyne
11
Niezła odpowiedź. Zdecydowanie coś do przemyślenia ... ale czy to, co mówisz, nie sprowadza się do tego, aby nie używać klas abstrakcyjnych?
brianestey
32
+1 za samą Regułę: „Preferuj złożoną sieć prostych obiektów nad prostą siecią złożonych obiektów”.
David Glass
12

To, co robię dla klas abstrakcyjnych i interfejsów, to: Piszę test, który używa obiektu, ponieważ jest on konkretny. Ale zmienna typu X (X jest klasą abstrakcyjną) nie jest ustawiona w teście. Ta klasa testowa nie jest dodawana do zestawu testów, ale jej podklasy, które mają metodę setup, która ustawia zmienną na konkretną implementację X. W ten sposób nie powielam kodu testowego. Podklasy niewykorzystanego testu mogą w razie potrzeby dodać więcej metod testowych.

Memento
źródło
czy to nie powoduje problemów z rzutowaniem w podklasie? jeśli X ma metodę a, a Y dziedziczy X, ale ma również metodę b. Kiedy podklasujesz swoją klasę testową, nie musisz rzutować swojej zmiennej abstrakcyjnej na Y, aby wykonać testy na b?
Johnno Nolan
8

Aby wykonać test jednostkowy konkretnie na klasie abstrakcyjnej, należy go wyprowadzić w celu testowania, przetestować wyniki base.method () i zamierzone zachowanie podczas dziedziczenia.

Testujesz metodę, wywołując ją, więc przetestuj klasę abstrakcyjną, implementując ją ...


źródło
8

Jeśli twoja klasa abstrakcyjna zawiera konkretną funkcjonalność, która ma wartość biznesową, wtedy zwykle przetestuję ją bezpośrednio, tworząc testową podwójną kopię, która usuwa dane abstrakcyjne, lub używając makiety, aby to dla mnie zrobić. To, który wybiorę, zależy w dużej mierze od tego, czy muszę pisać specyficzne dla testu implementacje metod abstrakcyjnych, czy nie.

Najczęstszym scenariuszem, w którym muszę to zrobić, jest użycie wzorca metody szablonowej , na przykład podczas tworzenia jakiegoś rozszerzalnego frameworka, który będzie używany przez firmę zewnętrzną. W tym przypadku klasa abstrakcyjna definiuje algorytm, który chcę przetestować, więc sensowniejsze jest testowanie bazy abstrakcyjnej niż konkretnej implementacji.

Uważam jednak, że ważne jest, aby testy te koncentrowały się wyłącznie na konkretnych implementacjach prawdziwej logiki biznesowej ; nie powinieneś testować jednostkowo szczegółów implementacji klasy abstrakcyjnej, ponieważ skończysz na kruchych testach.

Seth Petry-Johnson
źródło
6

Jednym ze sposobów jest napisanie abstrakcyjnego przypadku testowego, który odpowiada klasie abstrakcyjnej, a następnie napisanie konkretnych przypadków testowych, które podklasują abstrakcyjny przypadek testowy. zrób to dla każdej konkretnej podklasy oryginalnej klasy abstrakcyjnej (tj. hierarchia przypadków testowych odzwierciedla hierarchię klas). patrz Testowanie interfejsu w podręczniku przepisów kulinarnych Junit: http://safari.informit.com/9781932394238/ch02lev1sec6 .

zobacz także Testcase Superclass we wzorcach xUnit: http://xunitpatterns.com/Testcase%20Superclass.html

Ray Tayek
źródło
4

Argumentowałbym przeciwko „abstrakcyjnym” testom. Myślę, że test jest konkretnym pomysłem i nie ma abstrakcji. Jeśli masz wspólne elementy, umieść je w metodach pomocniczych lub klasach, z których mogą korzystać wszyscy.

Jeśli chodzi o testowanie abstrakcyjnej klasy testowej, zadaj sobie pytanie, co to jest testowana. Istnieje kilka podejść i powinieneś dowiedzieć się, co działa w twoim scenariuszu. Czy próbujesz przetestować nową metodę w swojej podklasie? Następnie pozwól, aby twoje testy współdziałały tylko z tą metodą. Czy testujesz metody w swojej klasie podstawowej? Wtedy prawdopodobnie mają osobne urządzenie tylko dla tej klasy i testują każdą metodę osobno za pomocą jak największej liczby testów.

casademora
źródło
Nie chciałem ponownie testować kodu, który już przetestowałem, dlatego właśnie szedłem drogą abstrakcyjnych przypadków testowych. Próbuję przetestować wszystkie konkretne metody w mojej klasie abstrakcyjnej w jednym miejscu.
Paul Whelan
7
Nie zgadzam się z wydobywaniem wspólnych elementów do klas pomocniczych, przynajmniej w niektórych (wielu?) Przypadkach. Jeśli klasa abstrakcyjna zawiera jakąś konkretną funkcjonalność, myślę, że całkowicie akceptowalne jest testowanie tej funkcji bezpośrednio przez jednostkę.
Seth Petry-Johnson
4

Jest to wzór, który zwykle stosuję podczas konfigurowania uprzęży do testowania klasy abstrakcyjnej:

public abstract class MyBase{
  /*...*/
  public abstract void VoidMethod(object param1);
  public abstract object MethodWithReturn(object param1);
  /*,,,*/
}

A wersja, której używam w testach:

public class MyBaseHarness : MyBase{
  /*...*/
  public Action<object> VoidMethodFunction;
  public override void VoidMethod(object param1){
    VoidMethodFunction(param1);
  }
  public Func<object, object> MethodWithReturnFunction;
  public override object MethodWithReturn(object param1){
    return MethodWihtReturnFunction(param1);
  }
  /*,,,*/
}

Jeśli abstrakcyjne metody zostaną wywołane, gdy się tego nie spodziewam, testy zakończą się niepowodzeniem. Podczas aranżacji testów mogę z łatwością wykasować metody abstrakcyjne za pomocą lambdas, które wykonują twierdzenia, zgłaszają wyjątki, zwracają różne wartości itp.


źródło
3

Jeśli konkretne metody wywołują dowolną z metod abstrakcyjnych, strategia nie będzie działać, a Ty chciałbyś przetestować zachowanie każdej klasy podrzędnej osobno. W przeciwnym razie rozszerzenie i zerwanie metod abstrakcyjnych zgodnie z opisem powinno być w porządku, pod warunkiem, że metody klas abstrakcyjnych zostaną oddzielone od klas potomnych.

Jeb
źródło
2

Przypuszczam, że mógłbyś chcieć przetestować podstawową funkcjonalność klasy abstrakcyjnej ... Ale prawdopodobnie najlepiej byłoby, gdybyś rozszerzył klasę bez nadpisywania żadnych metod i robił sobie drwiny z metod abstrakcyjnych.

As
źródło
2

Jedną z głównych motywacji korzystania z klasy abstrakcyjnej jest włączenie polimorfizmu w aplikacji - tzn. Można zastąpić inną wersję w czasie wykonywania. W rzeczywistości jest to prawie to samo, co używanie interfejsu, z wyjątkiem tego, że klasa abstrakcyjna zapewnia pewne typowe instalacje hydrauliczne, często określane jako wzorzec szablonu .

Z perspektywy testów jednostkowych należy wziąć pod uwagę dwie rzeczy:

  1. Interakcja klasy abstrakcyjnej z klasami pokrewnymi . Użycie fałszywego szkieletu testowego jest idealne w tym scenariuszu, ponieważ pokazuje, że twoja klasa abstrakcyjna dobrze się bawi z innymi.

  2. Funkcjonalność klas pochodnych . Jeśli masz niestandardową logikę napisaną dla klas pochodnych, powinieneś przetestować te klasy w izolacji.

edit: RhinoMocks to niesamowity fałszywy szkielet testowy, który może generować fałszywe obiekty w czasie wykonywania, dynamicznie wywodząc się z twojej klasy. Takie podejście pozwala zaoszczędzić niezliczone godziny ręcznych klas pochodnych.

bryanbcook
źródło
2

Po pierwsze, jeśli klasa abstrakcyjna zawierała jakąś konkretną metodę, myślę, że powinieneś to zrobić, biorąc pod uwagę ten przykład

 public abstract class A 

 {

    public boolean method 1
    {
        // concrete method which we have to test.

    }


 }


 class B extends class A

 {

      @override
      public boolean method 1
      {
        // override same method as above.

      }


 } 


  class Test_A 

  {

    private static B b;  // reference object of the class B

    @Before
    public void init()

      {

      b = new B ();    

      }

     @Test
     public void Test_method 1

       {

       b.method 1; // use some assertion statements.

       }

   }
shreeram banne
źródło
1

Jeśli klasa abstrakcyjna jest odpowiednia dla Twojej implementacji, przetestuj (jak sugerowano powyżej) pochodną klasę konkretną. Twoje założenia są prawidłowe.

Aby uniknąć nieporozumień w przyszłości, pamiętaj, że ta konkretna klasa testowa nie jest kpiną, lecz podróbką .

Ściśle mówiąc, próbę definiują następujące cechy:

  • Próbka jest używana zamiast każdej zależności badanej klasy przedmiotu.
  • Mock to pseudo-implementacja interfejsu (możesz sobie przypomnieć, że zgodnie z ogólną zasadą zależności powinny być deklarowane jako interfejsy; testowalność jest jednym z głównych powodów tego)
  • Zachowania członków interfejsu próbnego - niezależnie od tego, czy są to metody, czy właściwości - są dostarczane w czasie testu (ponownie, przy użyciu frameworku). W ten sposób unikasz łączenia testowanej implementacji z implementacją jej zależności (które powinny mieć własne testy dyskretne).
banduki
źródło
1

Po odpowiedzi na @ patrick-desjardins zaimplementowałem streszczenie i jego klasę implementacji wraz z @Testnastępującymi:

Klasa abstrakcyjna - ABC.java

import java.util.ArrayList;
import java.util.List;

public abstract class ABC {

    abstract String sayHello();

    public List<String> getList() {
        final List<String> defaultList = new ArrayList<>();
        defaultList.add("abstract class");
        return defaultList;
    }
}

Ponieważ nie można tworzyć instancji klas abstrakcyjnych, ale można je podklasować , konkretna klasa DEF.java wygląda następująco:

public class DEF extends ABC {

    @Override
    public String sayHello() {
        return "Hello!";
    }
}

Klasa @Test do testowania zarówno metody abstrakcyjnej, jak i nieabstrakcyjnej :

import org.junit.Before;
import static org.hamcrest.MatcherAssert.assertThat;
import static org.hamcrest.Matchers.empty;
import static org.hamcrest.Matchers.is;
import static org.hamcrest.Matchers.not;
import static org.hamcrest.Matchers.contains;
import java.util.Collection;
import java.util.List;
import static org.hamcrest.Matchers.equalTo;

import org.junit.Test;

public class DEFTest {

    private DEF def;

    @Before
    public void setup() {
        def = new DEF();
    }

    @Test
    public void add(){
        String result = def.sayHello();
        assertThat(result, is(equalTo("Hello!")));
    }

    @Test
    public void getList(){
        List<String> result = def.getList();
        assertThat((Collection<String>) result, is(not(empty())));
        assertThat(result, contains("abstract class"));
    }
}
Arpit Aggarwal
źródło