Подтвердить что ты не робот

Как написать тесты junit для интерфейсов?

Каков наилучший способ написания тестов 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:

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

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

4b9b3361

Ответ 1

Вопреки широко разрешенному ответу, который дал @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()}
        );
    }
}

Ответ 2

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

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

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

  • Используйте тестовые пакеты.

Ответ 3

Я также не согласен с 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
}

Ответ 4

Позднее дополнение к теме, совместное использование более новых решений

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

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

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

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

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

Ответ 5

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

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

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

Ответ 6

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

public interface MyInterfaceTest {
   public 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();
   };
}