Jak przetestować klasę abstrakcyjną w Javie za pomocą JUnit?

87

Jestem nowy w testowaniu Java z JUnit. Muszę pracować z Javą i chciałbym używać testów jednostkowych.

Mój problem jest taki: mam klasę abstrakcyjną z kilkoma metodami abstrakcyjnymi. Ale są metody, które nie są abstrakcyjne. Jak mogę przetestować tę klasę za pomocą JUnit? Przykładowy kod (bardzo prosty):

abstract class Car {

    public Car(int speed, int fuel) {
        this.speed = speed;
        this.fuel = fuel;
    }

    private int speed;
    private int fuel;

    abstract void drive();

    public int getSpeed() {
        return this.speed;
    }

    public int getFuel() {
        return this.fuel;
    }
}

Chcę przetestować getSpeed()i getFuel()funkcjonować.

Podobne pytanie do tego problemu jest tutaj , ale nie używa JUnit.

W dziale JUnit FAQ znalazłem ten link , ale nie rozumiem, co autor chce powiedzieć na tym przykładzie. Co oznacza ta linia kodu?

public abstract Source getSource() ;
Vasco
źródło
4
Zobacz stackoverflow.com/questions/1087339/… dla dwóch rozwiązań wykorzystujących Mockito.
ddso,
Czy jest jakaś korzyść, aby nauczyć się innej platformy do testowania? Czy Mockito to tylko rozszerzenie do jUnita, czy też zupełnie inny projekt?
Vasco
Mockito nie zastępuje JUnit. Podobnie jak inne frameworki do pozorowania, jest on używany jako dodatek do frameworka testowania jednostkowego i pomaga w tworzeniu pozorowanych obiektów do wykorzystania w przypadkach testowych.
ddso
1
Bez względu na

Odpowiedzi:

105

Jeśli nie masz konkretnych implementacji tej klasy, a metody nie są, staticjaki jest sens ich testowania? Jeśli masz konkretną klasę, będziesz testować te metody jako część publicznego interfejsu API konkretnej klasy.

Wiem, o czym myślisz „Nie chcę ciągle testować tych metod, dlatego stworzyłem klasę abstrakcyjną”, ale moim kontrargumentem jest to, że celem testów jednostkowych jest umożliwienie programistom wprowadzania zmian, uruchom testy i przeanalizuj wyniki. Część tych zmian może obejmować zastąpienie metod klasy abstrakcyjnej, zarówno protectedi public, jak , co może spowodować fundamentalne zmiany w zachowaniu. W zależności od charakteru tych zmian może to wpłynąć na działanie aplikacji w nieoczekiwany, prawdopodobnie negatywny sposób. Jeśli masz dobry zestaw testów jednostkowych, problemy wynikające z tego typu zmian powinny być widoczne w czasie programowania.

nsfyn55
źródło
17
100% pokrycia kodu to mit. Powinieneś mieć wystarczająco dużo testów, aby objąć wszystkie znane hipotezy dotyczące zachowania aplikacji (najlepiej napisane przed napisaniem kodu la Test Driven Development). Obecnie pracuję w bardzo dobrze funkcjonującym zespole TDD i mamy tylko 63% pokrycia od naszej ostatniej kompilacji, wszystkie napisane w miarę rozwoju. Czy to dobrze? kto wie ?, ale uważałbym za stratę czasu, aby wrócić i spróbować podnieść to wyżej.
nsfyn55
3
pewnie. Niektórzy twierdzą, że jest to naruszenie dobrego TDD. Wyobraź sobie, że jesteś w zespole. Zakładasz, że metoda jest ostateczna i nie umieszczasz testów w żadnych konkretnych implementacjach. Ktoś usuwa modyfikator i wprowadza zmiany, które wpływają na całą gałąź hierarchii dziedziczenia. Czy nie chciałbyś, aby Twój zestaw testów to wychwycił?
nsfyn55
31
Nie zgadzam się. Niezależnie od tego, czy pracujesz w TDD, czy nie, konkretna metoda Twojej klasy abstrakcyjnej zawiera kod, dlatego powinny mieć testy (niezależnie od tego, czy istnieją podklasy, czy nie). Ponadto testy jednostkowe w klasach testów Java (normalnie). Dlatego tak naprawdę nie ma logiki w testowaniu metod, które nie są częścią tej klasy, ale raczej jej superklasy. Zgodnie z tą logiką nie powinniśmy testować żadnej klasy w Javie, z wyjątkiem klas bez żadnych podklas. Jeśli chodzi o nadpisywane metody, to właśnie wtedy dodajesz test, aby sprawdzić zmiany / dodatki do testu podklasy.
ethanfar
3
@ nsfyn55 A co by było, gdyby konkretne metody były final? Nie widzę powodu do wielokrotnego testowania tej samej metody, jeśli implementacja nie może się zmienić
Dioxin
3
Czy nie powinniśmy mieć testów ukierunkowanych na abstrakcyjny interfejs, abyśmy mogli je uruchomić dla wszystkich implementacji? Jeśli nie jest to możliwe, naruszylibyśmy zasady Liskova, o czym chcielibyśmy wiedzieć i naprawić. Tylko jeśli implementacja dodaje jakąś rozszerzoną (kompatybilną) funkcjonalność, powinniśmy mieć dla niej określony test jednostkowy (i tylko dla tej dodatkowej funkcjonalności).
tne
36

Utwórz klasę konkretną, która dziedziczy klasę abstrakcyjną, a następnie przetestuj funkcje, które klasa konkretna dziedziczy z klasy abstrakcyjnej.

Kevin Bowersox
źródło
Co byś zrobił, gdybyś miał 10 klas konkretnych rozszerzających klasę abstrakcyjną, a każda z tych klas konkretnych implementowałaby tylko 1 metodę i powiedzmy, że pozostałe 2 metody są takie same dla każdej z tych klas, ponieważ są zaimplementowane w abstrakcji klasa? Mój przypadek jest taki, że nie chcę kopiować i wklejać testów dla klasy abstrakcyjnej do każdej podklasy.
scarface
12

Z przykładową klasą, którą opublikowałeś, nie ma sensu testowanie, getFuel()a getSpeed()ponieważ mogą one zwracać tylko 0 (nie ma ustawiania).

Jednak zakładając, że był to tylko uproszczony przykład dla celów ilustracyjnych i że masz uzasadnione powody do testowania metod w abstrakcyjnej klasie bazowej (inni już wskazali implikacje), możesz skonfigurować swój kod testowy tak, aby tworzył anonimowy podklasa klasy bazowej, która zapewnia tylko fikcyjne (bez operacji) implementacje dla metod abstrakcyjnych.

Na przykład w swoim TestCasemożesz to zrobić:

c = new Car() {
       void drive() { };
   };

Następnie przetestuj pozostałe metody, np .:

public class CarTest extends TestCase
{
    private Car c;

    public void setUp()
    {
        c = new Car() {
            void drive() { };
        };
    }

    public void testGetFuel() 
    {
        assertEquals(c.getFuel(), 0);
    }

    [...]
}

(Ten przykład jest oparty na składni JUnit3. W przypadku JUnit4 kod byłby nieco inny, ale idea jest taka sama).

Grodriguez
źródło
Dziękuję za odpowiedź. Tak, mój przykład był uproszczony (i niezbyt dobry). Po przeczytaniu wszystkich odpowiedzi tutaj napisałem atrapę klasy. Ale jak napisał @ nsfyn55 w swojej odpowiedzi, piszę test dla każdego potomka tej abstrakcyjnej klasy.
Vasco
9

Jeśli i tak potrzebujesz rozwiązania (np. Ponieważ masz zbyt wiele implementacji klasy abstrakcyjnej, a testowanie zawsze powtarzałoby te same procedury), możesz stworzyć abstrakcyjną klasę testową z abstrakcyjną metodą fabryczną, która zostanie usprawniona przez implementację tego klasa testowa. Ten przykład działa lub ja z TestNG:

Abstrakcyjna klasa testowa Car:

abstract class CarTest {

// the factory method
abstract Car createCar(int speed, int fuel);

// all test methods need to make use of the factory method to create the instance of a car
@Test
public void testGetSpeed() {
    Car car = createCar(33, 44);
    assertEquals(car.getSpeed(), 33);
    ...

Wdrożenie Car

class ElectricCar extends Car {

    private final int batteryCapacity;

    public ElectricCar(int speed, int fuel, int batteryCapacity) {
        super(speed, fuel);
        this.batteryCapacity = batteryCapacity;
    }

    ...

Klasa testu jednostkowego ElectricCarTestklasy ElectricCar:

class ElectricCarTest extends CarTest {

    // implementation of the abstract factory method
    Car createCar(int speed, int fuel) {
        return new ElectricCar(speed, fuel, 0);
    }

    // here you cann add specific test methods
    ...
thomas.mc.work
źródło
5

Mógłbyś zrobić coś takiego

public abstract MyAbstractClass {

    @Autowire
    private MyMock myMock;        

    protected String sayHello() {
            return myMock.getHello() + ", " + getName();
    }

    public abstract String getName();
}

// this is your JUnit test
public class MyAbstractClassTest extends MyAbstractClass {

    @Mock
    private MyMock myMock;

    @InjectMocks
    private MyAbstractClass thiz = this;

    private String myName = null;

    @Override
    public String getName() {
        return myName;
    }

    @Test
    public void testSayHello() {
        myName = "Johnny"
        when(myMock.getHello()).thenReturn("Hello");
        String result = sayHello();
        assertEquals("Hello, Johnny", result);
    }
}
iil
źródło
4

Utworzyłbym wewnętrzną klasę jUnit, która dziedziczy po klasie abstrakcyjnej. Można to utworzyć i mieć dostęp do wszystkich metod zdefiniowanych w klasie abstrakcyjnej.

public class AbstractClassTest {
   public void testMethod() {
   ...
   }
}


class ConcreteClass extends AbstractClass {

}
marting
źródło
3
To doskonała rada. Można by to jednak poprawić, podając przykład. Być może przykład klasy, którą opisujesz.
SDJMcHattie,
2

Możesz utworzyć instancję klasy anonimowej, a następnie przetestować tę klasę.

public class ClassUnderTest_Test {

    private ClassUnderTest classUnderTest;

    private MyDependencyService myDependencyService;

    @Before
    public void setUp() throws Exception {
        this.myDependencyService = new MyDependencyService();
        this.classUnderTest = getInstance();
    }

    private ClassUnderTest getInstance() {
        return new ClassUnderTest() {    
            private ClassUnderTest init(
                    MyDependencyService myDependencyService
            ) {
                this.myDependencyService = myDependencyService;
                return this;
            }

            @Override
            protected void myMethodToTest() {
                return super.myMethodToTest();
            }
        }.init(myDependencyService);
    }
}

Należy pamiętać, że widoczność musi dotyczyć protectedwłaściwości myDependencyServiceklasy abstrakcyjnej ClassUnderTest.

Możesz również zgrabnie połączyć to podejście z Mockito. Zobacz tutaj .

Samuel
źródło
2

Mój sposób na sprawdzenie tego jest dość prosty w każdym abstractUnitTest.java. Po prostu tworzę klasę w abstractUnitTest.java, która rozszerza klasę abstrakcyjną. I przetestuj to w ten sposób.

Haomin
źródło
0

Nie możesz przetestować całej klasy abstrakcyjnej. W tym przypadku masz metody abstrakcyjne, co oznacza, że ​​powinny być implementowane przez klasę rozszerzającą daną klasę abstrakcyjną.

W tej klasie programista musi napisać kod źródłowy przeznaczony dla jego logiki.

Innymi słowy, nie ma sensu testować klasy abstrakcyjnej, ponieważ nie jesteś w stanie sprawdzić jej ostatecznego zachowania.

Jeśli masz główną funkcjonalność niezwiązaną z metodami abstrakcyjnymi w jakiejś klasie abstrakcyjnej, po prostu utwórz inną klasę, w której metoda abstrakcyjna zgłosi wyjątek.

Damian Leszczyński - Vash
źródło
0

Opcjonalnie można utworzyć abstrakcyjną klasę testową obejmującą logikę wewnątrz klasy abstrakcyjnej i rozszerzyć ją dla każdego testu podklasy. W ten sposób możesz upewnić się, że ta logika zostanie przetestowana dla każdego dziecka osobno.

Andrew Taran
źródło