Тестируемый модуль: Impl или Interface?

Предположим, у меня есть интерфейс и класс реализации, который его реализует, и я хочу написать для этого модульный тест. Что я должен проверить интерфейс или 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() не является частью моего открытого интерфейса, поэтому я не хочуaccidentally назови это. Если я действительно хочу это назвать, я должен уделить минутку и подумать об этом. Это помогает мне, например, обнаружить, что я действительно пытаюсь сделать это внедрение зависимости. Это открывает для меня целый мир новых возможностей. Я могу использовать какой-то существующий механизм внедрения зависимостей (например, Spring), я могу имитировать его сам, как я это делал в своем коде, или использовать совершенно другой подход. Присмотритесь, подготовка PrintSream была не такой простой, может, мне вместо этого использовать фиктивный объект?

EDIT: I think I should always сосредоточиться на интерфейсе. С моей точки зренияsetTarget() не является частью "контракта" класса impl тоже нет, он служит для внедрения зависимостей. Я думаю, что любой открытый метод класса Impl должен рассматриваться как закрытый с точки зрения тестирования. Это не значит, что я игнорирую детали реализации.

Смотрите такжеДолжны ли частные / защищенные методы проходить модульное тестирование?

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

Ответы на вопрос(4)

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

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

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

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

 alexsmail08 июн. 2012 г., 11:05
Не могли бы вы привести последний пример? Я не понял твою точку зрения.

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

Требование вызова сеттера в модульном тесте (приведение интерфейса к реализации):

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

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

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

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

 alexsmail08 июн. 2012 г., 11:02
Я согласен с вами последнее замечание. Это моя точка зрения. Делая это, я вижу, что здесь потенциально что-то может быть не так. В случае множественной реализации \ нескольких интерфейсов я бы протестировал все реализации, но когда я объявил переменную в своем методе setUp (), я бы определенно использовал интерфейс.
 08 июн. 2012 г., 16:14
Я имел ввиду тестовую реализацию. Например: @Test public void isEqualsSymmetric () {assertEquals (new MyImplementation (), new MyImplementation ()); //Другого пути нет}
 08 июн. 2012 г., 12:43
Здесь мы рассмотрим пример только с одним простым сеттером. Но что, если вам нужно вызвать один из методов жизненного цикла бина в setUp () (приведение снова)? Что если вам нужно было протестировать метод #equals? Опять же, вам придется использовать конкретный тип (конкретную реализацию). В примерах, которые я упомянул, вам пришлось бы кастовать несколько раз. Я думаю, что это лучшее объяснение того, что тестирование в явном виде является лучшим подходом.

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

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

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

Прилипание к интерфейсу в ваших тестах также значительно облегчает насмешку.

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);
    }
}
 alexsmail07 июн. 2012 г., 20:54
Это не значит, что я игнорирую детали реализации.
 10 июн. 2012 г., 00:14
Независимо от того, где ваш ответ?
 08 июн. 2012 г., 14:30
Вы не можете больше ничего проверять.
 alexsmail07 июн. 2012 г., 20:47
Это можно считать еще одной реализацией моего интерфейса. :-) С точки зрения внедрения зависимостей это фактически спор о том, что лучше - конструктор-инъекция или сеттер-инъекция. Это не точка моего вопроса.
 09 июн. 2012 г., 21:39
@duffymo -1. Я думаю, что вы привели хорошую переработанную форму примера, но на самом деле не ответили на вопрос, так как вы просто спрятали проблему оп. Я предполагаю, что вы не утверждаете, что каждая реализация будет выставлять только сеттеры, отличные от интерфейса, если я прав, то вопрос о действии остается в силе.
Решение Вопроса

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

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

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

 alexsmail07 июн. 2012 г., 20:51
Я думаю, что я должен всегдаfocus на интерфейсе. С моей точки зренияsetTarget() не является частью "контракта" класса impl тоже нет, он служит для внедрения зависимостей. Я думаюany открытый метод класса Impl следует рассматривать как закрытый с точки зрения тестирования.

Ваш ответ на вопрос