Как писать тесты junit для интерфейсов? - PullRequest
67 голосов
/ 17 июля 2011

Каков наилучший способ написания тестов junit для интерфейсов, чтобы их можно было использовать для конкретных классов реализации?

например. У вас есть этот интерфейс и реализующие классы:

public interface MyInterface {
    /** Return the given value. */
    public boolean myMethod(boolean retVal);
}

public class MyClass1 implements MyInterface {
    public boolean myMethod(boolean retVal) {
        return retVal;
    }
}

public class MyClass2 implements MyInterface {
    public boolean myMethod(boolean retVal) {
        return retVal;
    }
}

Как бы вы написали тест для интерфейса, чтобы вы могли использовать его для класса?

Возможность 1:

public abstract class MyInterfaceTest {
    public abstract MyInterface createInstance();

    @Test
    public final void testMyMethod_True() {
        MyInterface instance = createInstance();
        assertTrue(instance.myMethod(true));
    }

    @Test
    public final void testMyMethod_False() {
        MyInterface instance = createInstance();
        assertFalse(instance.myMethod(false));
    }
}

public class MyClass1Test extends MyInterfaceTest {
    public MyInterface createInstance() {
        return new MyClass1();
    }
}

public class MyClass2Test extends MyInterfaceTest {
    public MyInterface createInstance() {
        return new MyClass2();
    }
}

Pro:

  • Нужен только один метод для реализации

Con:

  • Зависимости и фиктивные объекты тестируемого класса должны быть одинаковыми для всех тестов

Возможность 2:

public abstract class MyInterfaceTest
    public void testMyMethod_True(MyInterface instance) {
        assertTrue(instance.myMethod(true));
    }

    public void testMyMethod_False(MyInterface instance) {
        assertFalse(instance.myMethod(false));
    }
}

public class MyClass1Test extends MyInterfaceTest {
    @Test
    public void testMyMethod_True() {
        MyClass1 instance = new MyClass1();
        super.testMyMethod_True(instance);
    }

    @Test
    public void testMyMethod_False() {
        MyClass1 instance = new MyClass1();
        super.testMyMethod_False(instance);
    }
}

public class MyClass2Test extends MyInterfaceTest {
    @Test
    public void testMyMethod_True() {
        MyClass1 instance = new MyClass2();
        super.testMyMethod_True(instance);
    }

    @Test
    public void testMyMethod_False() {
        MyClass1 instance = new MyClass2();
        super.testMyMethod_False(instance);
    }
}

Pro:

  • точная грануляция для каждого теста, включая зависимости и фиктивные объекты

Con:

  • Каждый реализующий класс теста требует написания дополнительных методов теста

Какую возможность вы бы предпочли или каким другим способом вы пользуетесь?

Ответы [ 6 ]

71 голосов
/ 17 июля 2011

Вопреки часто задаваемому ответу, который дал @dlev, иногда бывает очень полезно / нужно написать тест, как вы предлагаете.Открытый API класса, выраженный через его интерфейс, является наиболее важной вещью для тестирования.При этом я бы не использовал ни один из упомянутых вами подходов, а вместо этого тест Parameterized , где параметры - это тестируемые реализации:

@RunWith(Parameterized.class)
public class InterfaceTesting {
    public MyInterface myInterface;

    public InterfaceTesting(MyInterface myInterface) {
        this.myInterface = myInterface;
    }

    @Test
    public final void testMyMethod_True() {
        assertTrue(myInterface.myMethod(true));
    }

    @Test
    public final void testMyMethod_False() {
        assertFalse(myInterface.myMethod(false));
    }

    @Parameterized.Parameters
    public static Collection<Object[]> instancesToTest() {
        return Arrays.asList(
                    new Object[]{new MyClass1()},
                    new Object[]{new MyClass2()}
        );
    }
}
18 голосов
/ 17 июля 2011

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

Итак, я знаю 2 решения.

  1. Реализация абстрактного теста с различными тестами, использующими интерфейс.Объявите абстрактный защищенный метод, который возвращает конкретный экземпляр.Теперь наследуйте этот абстрактный класс столько раз, сколько вам нужно для каждой реализации вашего интерфейса, и соответственно реализуйте упомянутый фабричный метод.Вы также можете добавить более конкретные тесты здесь.

  2. Использовать наборы тестов .

14 голосов
/ 18 июля 2011

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

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

@DataProvider
public Object[][] dp() {
  return new Object[][] {
    new Object[] { new MyImpl1() },
    new Object[] { new MyImpl2() },
  }
}

@Test(dataProvider = "dp")
public void f(MyInterface itf) {
  // will be called, with a different implementation each time
}
10 голосов
/ 06 августа 2015

Позднее добавление к теме, обмен информацией о новых решениях

Я также ищу правильный и эффективный способ проверки (на основе JUnit) правильности нескольких реализаций некоторых интерфейсов и абстрактных классов. К сожалению, ни тесты JUnit @Parameterized, ни эквивалентная концепция TestNG не соответствуют моим требованиям, поскольку я не знаю a priori списка реализаций этих интерфейсных / абстрактных классов, которые могут существовать. То есть могут быть разработаны новые реализации, и тестеры могут не иметь доступа ко всем существующим реализациям; поэтому неэффективно, чтобы тестовые классы определяли список классов реализации.

На данный момент я обнаружил следующий проект, который, кажется, предлагает полное и эффективное решение для упрощения этого типа тестов: https://github.com/Claudenw/junit-contracts. Это в основном позволяет определить «Контрактные тесты» через аннотацию @Contract(InterfaceClass.class) для контрактных тестовых классов. Затем разработчик создаст тестовый класс для конкретной реализации с аннотациями @RunWith(ContractSuite.class) и @ContractImpl(value = ImplementationClass.class); движок должен автоматически применять любой контрактный тест, который применяется к ExecutionClass, отыскивая весь Контрактный тест, определенный для любого интерфейса или абстрактного класса, из которого происходит урочный реализация. Я еще не тестировал это решение, но это звучит многообещающе.

Я также нашел следующую библиотеку: http://www.jqno.nl/equalsverifier/. Это удовлетворяет аналогичную, хотя и более конкретную потребность, которая заключается в утверждении соответствия класса конкретно контрактам Object.equals и Object.hashcode.

Аналогичным образом, https://bitbucket.org/chas678/testhelpers/src демонстрируют стратегию проверки некоторых Java-контрактов, включая Object.equals, Object.hashcode, Comparable.compare, Serializable. В этом проекте используются простые тестовые структуры, которые, я считаю, могут быть легко воспроизведены для удовлетворения любых конкретных потребностей.

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

5 голосов
/ 17 июля 2011

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

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

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

1 голос
/ 06 марта 2018

с Java 8, я делаю это

public abstract interface MyInterfaceTest {
   public abstract MyInterface createInstance();

   @Test
   default void testMyMethod_True() {
       MyInterface instance = createInstance();
       assertTrue(instance.myMethod(true));
   }

   @Test
   default void testMyMethod_False() {
       MyInterface instance = createInstance();
       assertFalse(instance.myMethod(false));
       }
   }

public class MyClass1Test implements MyInterfaceTest {
    public MyInterface createInstance() {
        return new MyClass1();
    }
}

public class MyClass2Test implements MyInterfaceTest {
   public MyInterface createInstance() {
       return new MyClass2();
   }

   @Disabled
   @Override
   @Test
   public void testMyMethod_True() {
       MyInterfaceTest.super.testMyMethod_True();
   };
}
Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...