Страницы

Поиск по вопросам

вторник, 24 декабря 2019 г.

юнит-тест для всех реализаций интерфейса

#java #юнит_тесты #интерфейс


Есть некоторый java интерфейс. Он накладывает ограничения на сигнатуры методов. Хотелось
бы еще ограничить возвращаемые значения некоторых методов или использование передаваемых
аргументов заранее во всех реализациях. Т.е. зафиксировать это ограничение поведения
в виде junit теста.

Например, есть

public interface Strategy
{
    int choose(List choiceList)
}


Метод choose должен выбирать только значение из передаваемого списка. Причем значения
в списке могут быть только от 0 до 9. Т.е. возвращаемое значение тоже должно быть от
0 до 9. Да, можно было бы использовать enum вместо int, но это лишь упрощенная задача.
Представьте, что может понадобится 100500 вариантов значений.

Можно ли написать junit тест, который будет запускаться для всех реализаций интерфейса,
находящихся в проекте? Что для этого нужно сделать, прописать? Может есть альтернатива
junit в данном вопросе?

Сам тест к интерфейсу из примера приводить не обязательно.
    


Ответы

Ответ 1



Тут уж, как я полагаю, придется каждую реализацию интерфейса отправлять на тестирование. Но а если все реализации должны работать одинаково, то я бы сделал весь необходимый ряд тест-кейсов, которые работают именно с интерфейсом. Ну а все реализации кропотливо скармливать туда. Приведу пример: abstract class StrategyTest { protected test(Strategy strategy) { test1(strategy); test2(strategy); } private test1(Strategy strategy){/* тест кейс 1 */ } private test2(Strategy strategy){/* тест кейс 2 */} } Ну и по всем реализациям останется как-то так сделать: class StrategyImplTest extends StrategyTest { @Test public void testCoose() throws Exception { Strategy s = new StrategyImpl(); super.test(s); } } Ну и так для каждой реализации будет класс с единственным тестом, который будет скармливать своему суперклассу объект для тестирования. Все assert'ы и так далее будут нормально отрабатывать и выбрасывать ошибку построения в мавене, если какой-то тест не сработает. Если же надо какие-то дополнительные аннотации для методов и т.д., можно сделать красивее. abstract class StrategyTest { @Test public void testCoose() throws Exception { Strategy s = getStrategy(); /* ваш код тестирования тут*/ } @Test public void testCoose1() throws Exception { Strategy s = getStrategy(); /* ваш код тестирования тут и так далее*/ } protected abstract Strategy getStrategy(); } Ну и по всем реализациям останется как-то так сделать: class StrategyImplTest extends StrategyTest { @Override public Strategy getStrategy() { Strategy s = new StrategyImpl(); return s; } } Я считаю, что исхищряться и придумывать гениальные алгоритмы с рефлексией или чем-то там еще, которые бы автоматом находили все реализации, не надо, иначе вы будете "Дартом Тестиусом" :) который сделает непонятные для всех тесты, которых будет сложно поддерживать.

Ответ 2



Если основная цель это ограничить входные и выходные значения методов всех реализаций интерфейса, то можно использовать AOP. В частности его реализацию в Spring. Общий смысл таков: в конфигурационном файле Spring указываем в каких случаях нам необходимо дополнить код метода проверочным кодом, который будет выбрасывать исключения при не соответствии с ожидаемыми значениями. Код advice который прицепляется к реализациям: public class StrategyCheckerAdvice { private static int MAX_CHOOSE_VAL = 15; private static int MAX_RET_VAL = 50; public void before(JoinPoint joinPoint) { for (Integer i : (List) joinPoint.getArgs()[0]) { if (i > MAX_CHOOSE_VAL) throw new IllegalArgumentException("Max value of choose is " + MAX_CHOOSE_VAL); } } public void afterReturning(Object retVal) throws Throwable { if ((Integer)retVal > MAX_RET_VAL) throw new IllegalArgumentException("Max ret val is " + MAX_RET_VAL); } } Конфигурационный файл Spring: Здесь бины strategy1 и strategy2 представляют собой имплементации тестируемого интерфейса. Метод choose первого возвращает int 1, а второго int 100. Так же имеется бин notStrategy, так же содержащий метод choose с той же сигнатурой, но при этом не реализующий интерфейс. Введен для того, чтобы показать, что advice присоединился только к нужным бинам и их методам. Весь тестовый проект доступен здесь.

Ответ 3



Возможно, я поспешил, выбрав для контроля поведения реализаций интерфейса универсальный юнит-тест. Основным в вопросе был первый абзац, остальное - это уже шаг в сторону решения. Мне пришел в голову вариант решения - переделать сигнатуру метода под использование своих типов данных, чтобы реализации интерфейса просто не могли нарушить желаемое поведение. В этом случае юнит-тесты могут оказаться не нужными. Можно переделать интерфейс: public interface Strategy { MyNumber choose(List choiceList) } А сам MyNumber переделать под соблюдение дополнительных условий. В моем случае пока пришлось сделать так: public final class MyNumber { private int n; private MyNumber(int n) { this.n = n; } static MyNumber[] enumerate(int count) { MyNumber[] numbers = new MyNumber[count]; for(int i = 0; i < count; i++) numbers[i] = new MyNumber(i); return numbers; } } Метод choose не сможет создать свои экземпляры MyNumber. Он может использовать только то, что ему передали. А диапазон передаваемых значений можно контролировать. Остается только возврат null, но это можно признать фичей :-) Есть ли какие замечания по этому решению?

Комментариев нет:

Отправить комментарий