Единица измерения: Impl или интерфейс?

Предположим, что у меня есть интерфейс и класс реализации, который его реализует, и я хочу написать unit-test для этого. Что я должен проверить интерфейс или Impl?

Вот пример:

public interface HelloInterface {
    public void sayHello();
}


public class HelloInterfaceImpl implements HelloInterface {
    private PrintStream target = System.out;


    @Override
    public void sayHello() {
        target.print("Hello World");

    }

    public void setTarget(PrintStream target){
        this.target = target;
    }
}

Итак, у меня есть HelloInterface и HelloInterfaceImpl, который его реализует. Что такое интерфейс с неполным тестированием или Impl?

Я думаю, что это должен быть HelloInterface. Рассмотрим следующий эскиз теста JUnit:

public class HelloInterfaceTest {
    private HelloInterface hi;

    @Before
    public void setUp() {
        hi = new HelloInterfaceImpl();
    }

    @Test
    public void testDefaultBehaviourEndsNormally() {
        hi.sayHello();
        // no NullPointerException here
    }

    @Test
    public void testCheckHelloWorld() throws Exception {
        ByteArrayOutputStream out = new ByteArrayOutputStream();
        PrintStream target = new PrintStream(out);
        PrivilegedAccessor.setValue(hi, "target", target);
        //You can use ReflectionTestUtils in place of PrivilegedAccessor
        //really it is DI 
        //((HelloInterfaceImpl)hi).setTarget(target);
        hi.sayHello();
        String result = out.toString();
        assertEquals("Hello World", result);

    }
 }

Основная строка - это тот, который я прокомментировал.

((HelloInterfaceImpl)hi).setTarget(target);

Метод setTarget() не является частью моего открытого интерфейса, поэтому я не хочу случайно его называть. Если я действительно хочу это назвать, я должен подумать об этом. Это помогает мне, например, обнаружить, что я действительно пытаюсь сделать инъекцию зависимости. Это открывает для меня весь мир новых возможностей. Я могу использовать какой-то существующий механизм инъекции зависимостей (например, Spring), я могу имитировать его сам, как это было на самом деле в моем коде, или использовать совершенно другой подход. Присмотритесь, подготовка PrintSream не так-то просто, может быть, я должен использовать mock-объект вместо этого?

ИЗМЕНИТЬ: Я думаю, что я всегда должен сосредоточиться на интерфейсе. С моей точки зрения, setTarget() не является частью "контракта" класса impl, и он служит для инъекции зависимостей. Я думаю, что любой публичный метод класса Impl следует считать приватным с точки зрения тестирования. Это не означает, что я игнорирую детали реализации.

См. также Должны ли частные/защищенные методы находиться под unit test?

EDIT-2 В случае нескольких реализаций\нескольких интерфейсов я бы тестировал все реализации, но когда я объявляю переменную в моем методе setUp(), я бы определенно использовал интерфейс.

Ответ 1

Реализация - это единица, которая должна быть протестирована. Это, конечно, то, что вы создаете, и что содержит программную/бизнес-логику.

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

Да, вероятно, было бы проще использовать Mockito для PrintStream, не всегда можно избежать использования макетного объекта, как в этом конкретном примере.

Ответ 2

Я бы тестировал интерфейс.

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

Это простой случай. Я бы предположил, что более сложный будет иметь factory для создания различных, более сложных реализаций интерфейса. Надеюсь, вы не разработали бы это так, чтобы вы были в коробке.

Привязка к интерфейсу в ваших тестах делает насмешливым намного проще.

public class HelloInterfaceImpl implements HelloInterface {

    private PrintStream target;

    public HelloInterfaceImpl() {
        this(System.out);
    }

    public HelloInterfaceImpl(PrintStream ps) { 
       this.target = ps;
    }

    @Override
    public void sayHello() {
        target.print("Hello World");
    }
}

Здесь тест:

public class HelloInterfaceTest {

    @Test
    public void testDefaultBehaviourEndsNormally() {
        HelloInterface hi = new HelloInterfaceImpl();    
        hi.sayHello();
    }

    @Test
    public void testCheckHelloWorld() throws Exception {
        ByteArrayOutputStream out = new ByteArrayOutputStream();
        PrintStream target = new PrintStream(out);
        HelloInterface hi = new HelloInterfaceImpl(target);    
        hi.sayHello();
        String result = out.toString();
        assertEquals("Hello World", result);
    }
}

Ответ 3

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

Требование вызова setter в unit test (литье интерфейса к реализации):

((HelloInterfaceImpl)hi).setTarget(target);

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

Возьмем пример из JDK. У вас есть интерфейс List и две реализации: ArrayList и LinkedList. Дополнительно LinkedList реализует интерфейс Deque. Если вы напишете тест для интерфейса List, что бы вы затронули? Массив или связанный список? Что еще в случае LinkedList, какой интерфейс вы бы выбрали для тестирования? Deque или List? Как вы видите, при тестировании реализаций у вас нет таких проблем.

Для меня лично листинг интерфейса для реализации в unit test является очевидным признаком того, что что-то идет не так;)

Ответ 4

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

В конечном счете вы тестируете реализацию.

В простом случае, как вы определили, я говорю шесть из полутора десятков других. Напишите свои тестовые примеры для интерфейса или реализации, если он достаточно проверяет реализацию, результаты одинаковы.

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