Как протестировать абстрактный класс в Java с помощью JUnit?


87

Я новичок в тестировании Java с помощью JUnit. Мне нужно работать с Java, и я хотел бы использовать модульные тесты.

Моя проблема: у меня есть абстрактный класс с некоторыми абстрактными методами. Но есть методы, которые не являются абстрактными. Как я могу протестировать этот класс с помощью JUnit? Пример кода (очень простой):

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

Хочу протестировать getSpeed()и getFuel()функции.

Аналогичный вопрос к этой проблеме здесь , но он не использует JUnit.

В разделе часто задаваемых вопросов JUnit я нашел эту ссылку , но не понимаю, что автор хочет сказать этим примером. Что означает эта строка кода?

public abstract Source getSource() ;

4
См. Stackoverflow.com/questions/1087339/… для двух решений с использованием Mockito.
ddso

Есть ли преимущество в изучении другого фреймворка для тестирования? Mockito - это только расширение jUnit или совершенно другой проект?
vasco

Mockito не заменяет JUnit. Как и другие фреймворки имитации, он используется в дополнение к фреймворку модульного тестирования и помогает создавать фиктивные объекты для использования в ваших тестовых примерах.
ddso

Ответы:


104

Если у вас нет конкретных реализаций класса и методы не имеют staticсмысла их тестировать? Если у вас есть конкретный класс, вы будете тестировать эти методы как часть общедоступного API конкретного класса.

Я знаю, что вы думаете: «Я не хочу тестировать эти методы снова и снова, поэтому я создал абстрактный класс», но мой контраргумент заключается в том, что суть модульных тестов заключается в том, чтобы позволить разработчикам вносить изменения, запустить тесты и проанализировать результаты. Частично эти изменения могут включать переопределение методов вашего абстрактного класса, как protectedи public, что может привести к фундаментальным изменениям в поведении. В зависимости от характера этих изменений это может повлиять на работу вашего приложения неожиданными, возможно, отрицательными способами. Если у вас есть хороший комплект модульного тестирования, проблемы, возникающие из-за изменений этих типов, должны быть очевидны во время разработки.


17
100% покрытие кода - это миф. У вас должно быть ровно столько тестов, чтобы охватить все ваши известные гипотезы о том, как должно вести себя ваше приложение (желательно, написанные до того, как вы напишете код la Test Driven Development). В настоящее время я работаю над очень хорошо функционирующей командой TDD, и у нас есть только 63% покрытия на момент нашей последней сборки, все написано по мере разработки. Это хорошо? кто знает ?, но я считаю пустой тратой времени возвращаться и пытаться поднять это выше.
nsfyn55

3
конечно. Некоторые утверждают, что это нарушение хорошего TDD. Представьте, что вы в команде. Вы делаете предположение, что метод окончательный и не ставите тесты для каких-либо конкретных реализаций. Кто-то удаляет модификатор и вносит изменения, которые распространяются по всей ветви иерархии наследования. Разве вы не хотите, чтобы ваш набор тестов это уловил?
nsfyn55 05

31
Я не согласен. Независимо от того, работаете ли вы в TDD или нет, конкретный метод вашего абстрактного класса содержит код, поэтому у них должны быть тесты (независимо от того, есть ли подклассы или нет). Более того, модульное тестирование в Java тестирует (обычно) классы. Следовательно, действительно нет логики в методах тестирования, которые не являются частью класса, а являются его суперклассом. Следуя этой логике, мы не должны тестировать какой-либо класс в Java, кроме классов, вообще не имеющих подклассов. Что касается переопределяемых методов, то именно тогда, когда вы добавляете тест для проверки изменений / дополнений в тесте подкласса.
ethanfar

3
@ nsfyn55 А если бы конкретные методы были final? Я не вижу причин для тестирования одного и того же метода несколько раз, если реализация не может измениться
Диоксин

3
Разве тесты не должны быть нацелены на абстрактный интерфейс, чтобы мы могли запускать их для всех реализаций? Если это невозможно, мы нарушим Лисков, о котором мы хотим знать и исправить. Только если реализация добавляет некоторые расширенные (совместимые) функции, у нас должен быть специальный модульный тест для нее (и только для этой дополнительной функции).
tne

36

Создайте конкретный класс, который наследует абстрактный класс, а затем проверьте функции, которые конкретный класс наследует от абстрактного класса.


Что бы вы сделали, если у вас есть 10 конкретных классов, расширяющих абстрактный класс, и каждый из этих конкретных классов будет реализовывать только 1 метод, и скажем, что другие 2 метода одинаковы для каждого из этих классов, потому что они реализованы в абстрактном класс? В моем случае я не хочу копировать и вставлять тесты для абстрактного класса в каждый подкласс.
scarface

12

В примере класса, который вы опубликовали, похоже, нет смысла тестировать, getFuel()и getSpeed()поскольку они могут возвращать только 0 (нет сеттеров).

Однако, предполагая, что это был просто упрощенный пример для иллюстративных целей и что у вас есть законные причины для тестирования методов в абстрактном базовом классе (другие уже указали на последствия), вы можете настроить свой тестовый код так, чтобы он создавал анонимный подкласс базового класса, который просто предоставляет фиктивные (безоперационные) реализации абстрактных методов.

Например, в вашем TestCaseвы можете сделать это:

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

Затем проверьте остальные методы, например:

public class CarTest extends TestCase
{
    private Car c;

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

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

    [...]
}

(Этот пример основан на синтаксисе JUnit3. Для JUnit4 код будет немного другим, но идея та же.)


Спасибо за ответ. Да, мой пример был упрощен (и не очень хорош). Прочитав здесь все ответы, я написал фиктивный класс. Но, как написал @ nsfyn55 в своем ответе, я пишу тест для каждого потомка этого абстрактного класса.
vasco

9

Если вам все равно нужно решение (например, потому что у вас слишком много реализаций абстрактного класса и тестирование всегда будет повторять одни и те же процедуры), вы можете создать абстрактный тестовый класс с абстрактным фабричным методом, который будет выполняться реализацией этого тестовый класс. Эти примеры работают или я с TestNG:

Абстрактный тестовый класс 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);
    ...

Реализация Car

class ElectricCar extends Car {

    private final int batteryCapacity;

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

    ...

Модульный тестовый класс ElectricCarTestКласса 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
    ...

5

Вы могли бы сделать что-то вроде этого

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);
    }
}

4

Я бы создал внутренний класс jUnit, унаследованный от абстрактного класса. Он может быть создан и иметь доступ ко всем методам, определенным в абстрактном классе.

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


class ConcreteClass extends AbstractClass {

}

3
Это отличный совет. Однако это можно улучшить, приведя пример. Возможно, это пример класса, который вы описываете.
SDJMcHattie

2

Вы можете создать экземпляр анонимного класса, а затем протестировать этот класс.

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);
    }
}

Имейте в виду, что видимость должна быть protectedдля свойства myDependencyServiceабстрактного класса ClassUnderTest.

Вы также можете аккуратно комбинировать этот подход с Mockito. Смотрите здесь .


2

Мой способ проверить это довольно просто, в каждом abstractUnitTest.java. Я просто создаю класс в abstractUnitTest.java, который расширяет абстрактный класс. И протестируйте это таким образом.


0

Вы не можете протестировать весь абстрактный класс. В этом случае у вас есть абстрактные методы, это означает, что они должны быть реализованы классом, расширяющим данный абстрактный класс.

В этом классе программист должен написать исходный код, предназначенный для его логики.

Другими словами, нет смысла тестировать абстрактный класс, потому что вы не можете проверить его окончательное поведение.

Если у вас есть основные функции, не связанные с абстрактными методами в каком-то абстрактном классе, просто создайте другой класс, в котором абстрактный метод вызовет какое-то исключение.


0

Как вариант, вы можете создать абстрактный тестовый класс, охватывающий логику внутри абстрактного класса, и расширить его для каждого теста подкласса. Таким образом, вы можете убедиться, что эта логика будет проверяться для каждого ребенка отдельно.

Используя наш сайт, вы подтверждаете, что прочитали и поняли нашу Политику в отношении файлов cookie и Политику конфиденциальности.
Licensed under cc by-sa 3.0 with attribution required.