Jak testować klasy abstrakcyjne: rozszerzać za pomocą stubów?

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

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

Czy powinienem mieć abstrakcyjny przypadek testowy, który może być użyty do przetestowania metod klasy abstrakcyjnej i rozszerzenia tej klasy w moim przypadku testowym dla obiektów rozszerzających klasę abstrakcyjną?

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

Author: Arpit, 2008-10-28

14 answers

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

Powinieneś przetestować klasę abstrakcyjną, która zawiera logikę, tak jak wszystkie inne klasy, które posiadasz.

 231
Author: Patrick Desjardins,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2010-03-12 05:26:34

Są dwa sposoby użycia abstrakcyjnych klas bazowych.

  1. Specjalizujesz się w swoim abstrakcyjnym obiekcie, ale wszyscy klienci będą używać klasy pochodnej poprzez jej podstawowy interfejs.

  2. Używasz abstrakcyjnej klasy bazowej do uwzględniania powielania w obiektach w Twoim projekcie, a klienci używają konkretnych implementacji za pośrednictwem własnych interfejsów.!


Rozwiązanie Dla 1-Strategii Wzór

Opcja1

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

Powinieneś rozważyć uczynienie z tego prawdziwego interfejsu, zmianę klasy abstrakcyjnej na konkretną i wziąć przykład tego interfejsu w jego konstruktorze. Twoje pochodne klasy stają się implementacjami tego nowego interfejsu.

IMotor

Oznacza to, że możesz teraz przetestuj swoją wcześniej abstrakcyjną klasę używając przykładowej instancji nowego interfejsu, a każda nowa implementacja poprzez teraz publiczny interfejs. Wszystko jest proste i testowalne.


Roztwór Dla 2

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

AbstractHelper

Spójrz na funkcjonalność, którą zawiera. Sprawdź, czy coś z tego może być popchnięte na obiekty, które są manipulowane, aby zminimalizować to duplikacja. Jeśli nadal coś ci zostało, spójrz na uczynienie z niej klasy pomocniczej, którą twoja konkretna implementacja przyjmuje do swojego konstruktora i usuwa ich klasę bazową.

Pomocnik Motorowy

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


As a Rule

Faworyzuje 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.


Aktualizacja: Jak radzić sobie z mieszankami obu?

Możliwe jest posiadanie klasy bazowej wykonującej obie te role... ie: posiada publiczny interfejs i posiada chronione metody pomocnicze. Jeśli tak jest, wtedy możesz uwzględnić metody pomocnicze w jedną klasę (scenario2) i przekształcić drzewo dziedziczenia w wzorzec strategii.

Jeśli okaże się, że masz jakieś metody, które Twoja klasa bazowa implementuje bezpośrednio, a inne są wirtualne, to nadal możesz przekształć drzewo dziedziczenia w wzorzec strategii, ale chciałbym również wziąć to jako dobry wskaźnik, że obowiązki nie są prawidłowo wyrównane i może wymagać refaktoryzacji.


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

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

Mamy standardowy format dla naszych plików konfiguracyjnych. To konkretne narzędzie ma 3 pliki konfiguracyjne w tym formacie. Chciałem mieć mocno wpisaną klasę dla każdego pliku ustawień, aby poprzez iniekcję zależności, Klasa mogła poprosić o ustawienia, na których jej zależało.

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

Mogłem napisać "SettingsFileParser", że 3 klasy zawinięte, a następnie delegowane do klasy bazowej, aby odsłonić metody dostępu do danych. Zdecydowałem się nie robić tego jeszcze, ponieważ doprowadziłoby to do 3 pochodnych klas z większą delegacją kodu w nich niż cokolwiek innego.

Jednak... w miarę rozwoju tego kodu, konsumenci każdej z tych klas ustawień stają się jaśniejsi. Każdy ustawienia użytkownik poprosi o niektóre ustawienia i przekształcić je w jakiś sposób(jak ustawienia są tekst mogą zawijać je w obiekty konwertować je do liczb itp.). Jak to się stanie, zacznę wyciągać tę logikę do metod manipulacji danymi i wypchnąć je z powrotem na silnie wpisane klasy ustawień. Doprowadzi to do wyższego poziomu interfejsu dla każdego zestawu ustawień, który ostatecznie nie jest już świadomy, że ma do czynienia z "ustawieniami".

W tym momencie klasy silnie wpisanych ustawień nie będą już potrzebować metod "getter", które ujawniają podstawową implementację 'settings'.

W tym momencie nie chciałbym już, aby ich publiczny interfejs zawierał metody accessor settings; więc Zmienię tę klasę na encapsulate a settings Parser class zamiast derive from it.

Klasa abstrakcyjna jest zatem: sposobem na uniknięcie kodu delegacyjnego w tej chwili i znacznikiem w kodzie, który przypomni mi o późniejszej zmianie projektu. Może nigdy do tego nie dotrę, więc może trochę pożyć... tylko kod może powiedzieć.

Uważam, że jest to prawda z każdą regułą... jak "brak metod statycznych" lub "brak metod prywatnych". Wskazują na zapach w kodzie... i to dobrze. Utrzymuje szukasz abstrakcji, której ci brakowało... i pozwala kontynuować dostarczanie wartości do Klienta w średnim czasie.

Wyobrażam sobie reguły takie jak ta, definiujące krajobraz, w którym utrzymywalny kod żyje w dolinach. Gdy dodajesz nowe zachowanie, to jak deszcz lądujący na Twoim kodzie. Początkowo kładziesz go tam, gdzie wyląduje.. następnie refaktorujesz, aby pozwolić siłom dobrego projektu popchnąć zachowanie, aż wszystko skończy się w dolinach.

 402
Author: Nigel Thorne,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2014-06-20 12:12:04

To, co robię dla klas abstrakcyjnych i interfejsów, jest następujące: piszę test, który wykorzystuje obiekt tak, jak jest konkretny. Ale zmienna typu X (X jest klasą abstrakcyjną) nie jest ustawiana w teście. Ta klasa testowa nie jest dodawana do pakietu testowego, 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 nieużywanego testu mogą w razie potrzeby dodać więcej metod testowych.

 11
Author: Mnementh,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2009-04-15 11:43:29

Aby wykonać test jednostkowy specjalnie na klasie abstrakcyjnej, należy go wyprowadzić do celów testowych, test base.metoda() wyniki i zamierzone zachowanie podczas dziedziczenia.

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

 8
Author: ,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2008-10-28 13:39:22

Jeśli twoja klasa abstrakcyjna zawiera konkretną funkcjonalność, która ma wartość biznesową, to zwykle przetestuję ją bezpośrednio, tworząc test double, który usuwa abstrakcyjne dane, lub używając wyśmiewającego frameworka, aby to zrobić za mnie. To, który z nich wybieram, zależy w dużej mierze od tego, czy muszę pisać implementacje abstrakcyjnych metod testowych, czy też nie.

Najczęstszym scenariuszem, w którym muszę to zrobić, jest użycie wzorca metody szablonowej , np. gdy budowanie pewnego rodzaju rozszerzalnych ram, które będą używane przez stronę trzecią. W tym przypadku klasa abstrakcyjna jest tym, co definiuje algorytm, który chcę przetestować, więc bardziej sensowne jest testowanie abstrakcyjnej bazy niż konkretnej implementacji.

Jednak myślę, że ważne jest, aby te testy koncentrowały się na konkretnych implementacjach prawdziwej logiki biznesowej tylko ; nie powinieneś testować jednostkowych szczegółów implementacji klasy abstrakcyjnej, ponieważ skończysz z testy kruchości.

 8
Author: Seth Petry-Johnson,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2008-10-28 14:19:18

Jednym ze sposobów jest napisanie abstrakcyjnego przypadku testowego, który odpowiada twojej klasie abstrakcyjnej, a następnie napisanie konkretnych przypadków testowych, które podklasują Twój abstrakcyjny przypadek testowy. zrób to dla każdej konkretnej podklasy oryginalnej klasy abstrakcyjnej(tzn. hierarchia przypadków testowych odzwierciedla hierarchię klas). zobacz testowanie interfejsu w książce junit: http://safari.informit.com/9781932394238/ch02lev1sec6 .

Zobacz też Superclass Testcase we wzorcach xUnit: http://xunitpatterns.com/Testcase%20Superclass.html

 6
Author: Ray Tayek,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2008-12-04 07:41:05

Przeciwstawiłbym się testom "abstrakcyjnym". Myślę, że test to konkretny pomysł i nie ma abstrakcji. Jeśli masz wspólne elementy, umieść je w metodach pomocniczych lub klasach, z których każdy może korzystać.

Jeśli chodzi o testowanie abstrakcyjnej klasy testowej, upewnij się, że zadajesz sobie pytanie, co to jest testowanie. Istnieje kilka podejść i powinieneś dowiedzieć się, co działa w Twoim scenariuszu. Próbujesz przetestować nową metodę w swojej podklasie? Więc niech twoje testy będą tylko z tym współdziałać. metoda. Testujesz metody w swojej klasie podstawowej? Następnie prawdopodobnie mieć oddzielne urządzenie tylko dla tej klasy, i przetestować każdą metodę indywidualnie z tak wielu testów, jak to konieczne.

 4
Author: casademora,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2008-10-28 13:35:55

Jest to wzór, którym zwykle kieruję się podczas tworzenia uprzęży do testowania klasy abstrakcyjnej:

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

I 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 metody abstrakcyjne są wywoływane, gdy się tego nie spodziewam, testy zawodzą. Podczas układania testów, mogę łatwo wymazać abstrakcyjne metody z lambda, które wykonują twierdzenia, rzucają wyjątki, zwracają różne wartości itp.

 4
Author: Will,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2008-10-28 14:03:46

Jeśli konkretne metody odwołują się do którejkolwiek z abstrakcyjnych metod, strategia nie będzie działać i chciałbyś przetestować zachowanie każdej klasy dziecka osobno. W przeciwnym razie, Rozszerzanie go i stubowanie metod abstrakcyjnych, jak już opisałeś, powinno być w porządku, ponownie pod warunkiem, że konkretne metody klasy abstrakcyjnej są oddzielone od klas potomnych.

 3
Author: Jeb,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2008-10-28 13:36:38

Przypuszczam, że mógłbyś chcieć przetestować podstawową funkcjonalność klasy abstrakcyjnej... Ale prawdopodobnie najlepiej byłoby rozszerzyć klasę bez nadpisywania jakichkolwiek metod i zrobić minimum wysiłku wyśmiewając metody abstrakcyjne.

 2
Author: Ace,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2008-10-28 13:36:50

Jedną z głównych motywacji do używania klasy abstrakcyjnej jest włączenie polimorfizmu w Twojej aplikacji-czyli: możesz zastąpić inną wersję w czasie wykonywania. W rzeczywistości jest to bardzo to samo, co używanie interfejsu, z wyjątkiem klasy abstrakcyjnej, która zapewnia pewną wspólną instalację wodną, często określaną jako wzorzec szablonu .

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

  1. Interakcja klasy abstrakcyjnej z nią podobne klasy . Używanie wzorcowego frameworka testowego jest idealne dla tego scenariusza, ponieważ pokazuje, że Twoja abstrakcyjna klasa dobrze gra z innymi.

  2. Funkcjonalność klas pochodnych . Jeśli masz niestandardową logikę, którą napisałeś dla swoich klas pochodnych, powinieneś przetestować te klasy w izolacji.

Edit: RhinoMocks jest niesamowitym frameworkiem do testowania makiet, który może generować makiety obiektów w czasie wykonywania, dynamicznie czerpiąc z twojej klasy. To podejście pozwala zaoszczędzić niezliczone godziny ręcznie kodowanych klas pochodnych.

 2
Author: bryanbcook,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2008-10-28 14:09:27

Po pierwsze, jeśli klasa abstrakcyjna zawierała jakąś konkretną metodę, myślę, że powinieneś to zrobić rozważając 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.

       }

   }
 2
Author: shreeram banne,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2015-05-16 06:32:51

Po odpowiedzi @patrick-desjardins zaimplementowałem abstract i jego klasę implementacji wraz z @Test następująco:

Klasa Abstract-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;
    }
}

Jako klasy abstrakcyjne nie mogą być tworzone jako instancje, ale mogą być podklasowane, klasa betonu DEF.java , jest następująca:

public class DEF extends ABC {

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

@Test klasa do testowania zarówno metody abstrakcyjnej, jak i nieabstraktowej:

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"));
    }
}
 1
Author: Arpit,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2017-01-23 16:06:34

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

Aby uniknąć przyszłego zamieszania, należy pamiętać, że ta konkretna Klasa testowa nie jest makietą, ale fałszywą .

W ścisłym znaczeniu makieta jest zdefiniowana przez następujące cechy:

  • mock jest używany w miejsce każdej zależności testowanej klasy.
  • makieta to pseudo-implementacja interfejsu (można przypomnieć, że ogólnie zależności powinny być deklarowane jako interfejsy; testowalność jest jednym z głównych powodów tego)
  • zachowania członków interfejsu mocka - czy metody czy właściwości -- są dostarczane w czasie testu (ponownie, za pomocą ramy szyderczej). W ten sposób unikniesz łączenia testowanej implementacji z implementacją jej zależności (które powinny mieć własne testy dyskretne).
 0
Author: banduki,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2018-01-31 16:38:32