Як написати junit-тести для інтерфейсів?


77

Який найкращий спосіб написати 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();
    }
}

Про:

  • Потрібен лише один метод для реалізації

Недолік:

  • Залежності та фіктивні об'єкти класу, що тестується, повинні бути однаковими для всіх тестів

Можливість 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);
    }
}

Про:

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

Недолік:

  • Кожен клас реалізації тесту вимагає написання додаткових методів тестування

Якій можливості ви віддасте перевагу чи який інший спосіб ви використовуєте?


Можливості 1 недостатньо, коли конкретний клас знаходиться в іншому пакеті, компоненті або команді розробників.
Andy Thomas

1
@AndyThomas: Чому ти так кажеш? Я використовую можливість 1 із конкретними класами (як для реалізації, так і для тестів) у різних пакетах та проектах Maven.
Тревор Робінсон,

1
@TrevorRobinson - Подумавши про цей трирічний коментар, на даний момент я можу подумати лише про те, що класи поза вашим контролем можуть мати кілька конструкторів, але можливість 1 запускає кожен тест на об’єкті, створеному лише з одним із них.
Енді Томас,

З опцією 1 ви можете мати окремі методи @Before у кожному конкретному класі. Ви також можете проводити одноразові тести на заняттях з бетону, як вам зручно.
JoshOrndorff

Відповіді:


79

На відміну від багатоголосної відповіді, яку дав @dlev, іноді може бути дуже корисно / потрібно написати тест, як ви пропонуєте. Загальнодоступний API класу, виражений через його інтерфейс, є найважливішим для перевірки. З огляду на це, я б не використовував жоден із згаданих вами підходів, а замість цього параметризований тест, де параметри є реалізаціями, які потрібно перевірити:

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

4
Здається, існує проблема з таким підходом. Той самий екземпляр MyClass1 та MyClass2 використовується для виконання всіх методів тестування. В ідеалі кожен testMethod повинен виконуватися з новим екземпляром MyClass1 / MyClass2, цей недолік робить цей підхід непридатним для використання.
ChrisOdney

12
Якщо вам потрібен новий примірник приладу для кожного тестового методу, тоді нехай метод параметрів повертає завод, який кожен тест викликає, щоб отримати свій прилад. Це не впливає на життєздатність цього підходу.
Райан Стюарт,

1
а як щодо розміщення посилання на клас до параметрів, а в методі "InterfaceTesting" створюється екземпляр із відображенням?
ArcanisCz

2
@ArcanisCz: Як і у випадку з коментатором eariler, те , як змусити екземпляр тестуватися, не так важливо. Важливим моментом є те, що якийсь параметризований тест, ймовірно, є правильним підходом.
Райан Стюарт,

4
Припустимо, я пишу інтерфейс, надаю кілька реалізацій та тест, написаний таким чином. Якщо користувач створює нову реалізацію і хоче її протестувати, він повинен змінити вихідний код тесту. Це змушує мене вважати абстрактний метод, що повертає екземпляр тестування, більш корисним, особливо якщо ви очікуєте, що ваші клієнти створять власні реалізації.
jspurim

20

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

Отже, я знаю 2 рішення.

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

  2. Використовуйте тестові набори .


14

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

Хороша відповідь. Схоже, testng хороший механізм для цього.
fastcodejava

13

Пізнє додавання до теми, обмін новими ідеями про рішення

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

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

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

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

Ну, це поки що; Я буду тримати цю публікацію в курсі іншої корисної інформації, яку я можу знайти.


6

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

Блокові тести, навпаки, призначені для того, щоб переконатися, що функціональність, яку ви очікуєте, присутня в заданому шляху коду.

З огляду на це, існують ситуації, коли такий тип тесту може мати сенс. Якщо припустити, що ви хотіли, щоб ці тести гарантували, що написані вами класи (які мають спільний даний інтерфейс) насправді мають однакову функціональність, тоді я віддав би перевагу першому варіанту. Це робить найпростішим у реалізації підкласів введення себе в процес тестування. Крім того, я не думаю, що ваша "кон" справді справжня. Немає жодної причини, коли ви не можете, щоб класи, які фактично тестуються, надавали свої власні макети (хоча я думаю, що якщо вам дійсно потрібні різні макети, то це припускає, що ваші тести інтерфейсу в будь-якому випадку не є однорідними)


27
Інтерфейс не визначає функціональність, але він визначає API, якого його реалізації повинні дотримуватися. Ось на чому повинен зосередитися тест, то чому б не написати тесту, щоб висловити це? Зокрема, коли ви добре використовуєте поліморфізм із кількома реалізаціями інтерфейсу, такий тест є надзвичайно цінним.
Ryan Stewart,

2
Я згоден з dlev - я не бачу сенсу тестувати інтерфейс. Компілятор повідомить вам, якщо ваша конкретна реалізація не реалізує інтерфейс. Я не бачу в цьому жодної цінності. Одиничні тести призначені для бетонних класів.
duffymo

5
@Ryan - контракт на інтерфейс рідко буває "просто умовою". Контракт повідомляє тверді очікування одержувачів інтерфейсу. Компілятор може допускати порушення контракту, але часто спричиняє несподівану поведінку під час виконання. Одне визначення цього контракту в модульних тестах краще, ніж множинне.
Andy Thomas

1
Для людей, які не погоджуються з написанням модульних тестів для інтерфейсу - якщо для LinkedList існує кілька реалізацій, таких як Singly, Doubly, Circular тощо. Чому я повинен переписувати загальні модульні тести, чи не зручніше мати параметризований тест? @duffymo ти такий самий duffymo з форумів сонця?
ChrisOdney

5
Я б протестував через інтерфейс лише функціональність, яка є частиною контракту, наприклад: коли елемент додається до списку вперше, розмір повинен стати 1, незалежно від того, яка реалізація використовується, цей інваріант повинен мати. Однак додаткові тести можуть знадобитися для реалізацій, які перевіряють функціональність, характерну лише для цієї реалізації.
tsobe

1

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

ключове слово "абстрактне" зайве.
Ubuntix

Цей підхід більш детально задокументований у Посібнику користувача JUnit 5, у розділі Тестові інтерфейси та Методи за замовчуванням .
Герт-Ян Хат
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.