Mockito / Spring MVC - динамическая проверка соответствия запросов (на основе аннотаций) - PullRequest
2 голосов
/ 01 марта 2012

В настоящее время я пишу веб-приложение на основе Spring MVC.

Вместо того, чтобы писать по одному тесту для каждого аннотированного метода, я бы хотел воспользоваться параметризованным JUnit runner.

Наконец, я почти все заработал, хотя мне пришлось заменить все примитивные аргументы на их аналог-оболочку в моих методах контроллера (а затем вручную выполнить проверки работоспособности на нулевых ссылках).

Если это может помочь, вот код (это также зависит от Гуавы):

@RunWith(Parameterized.class)
public class MyControllerMappingTest {

    private MockHttpServletRequest request;
    private MockHttpServletResponse response;
    private MyController mockedController;
    private AnnotationMethodHandlerAdapter annotationHandlerAdapter;
    private final String httpMethod;
    private final String uri;
    private final String controllerMethod;
    private final Class<?>[] parameterTypes;
    private final Object[] parameterValues;

    @Before
    public void setup() {
        request = new MockHttpServletRequest();
        response = new MockHttpServletResponse();
        mockedController = mock(MyController.class);
        annotationHandlerAdapter = new AnnotationMethodHandlerAdapter();
    }

    @Parameters
    public static Collection<Object[]> requestMappings() {
        return asList(new Object[][] {
                {"GET", "/my/uri/0", "index", arguments(new MethodArgument(Integer.class, 0))}
        });
    }

    private static List<MethodArgument> arguments(MethodArgument... arguments) {
        return asList(arguments);
    }

    public MyControllerMappingTest(String httpMethod, String uri, String controllerMethod, List<MethodArgument> additionalParameters) {
        this.httpMethod = httpMethod;
        this.uri = uri;
        this.controllerMethod = controllerMethod;
        this.parameterTypes = new Class<?>[additionalParameters.size()];
        initializeParameterTypes(additionalParameters);
        this.parameterValues = newArrayList(transform(additionalParameters, valueExtractor())).toArray();
}

    private void initializeParameterTypes(List<MethodArgument> additionalParameters) {
        Iterable<Class<?>> classes = transform(additionalParameters, typeExtractor());
        int i = 0;
        for (Class<?> parameterClass : classes) {
            parameterTypes[i++] = parameterClass;
        }
    }

    @Test
    public void when_matching_mapping_constraints_then_controller_method_automatically_called() throws Exception {
        request.setMethod(httpMethod);
        request.setRequestURI(uri);
        annotationHandlerAdapter.handle(request, response, mockedController);

        Method method = MyController.class.getMethod(controllerMethod, parameterTypes);
        method.invoke(verify(mockedController), parameterValues);
    }
}

со следующим классом MethodArgument:

public class MethodArgument {
    private final Class<?> type;
    private final Object value;

    public MethodArgument(final Class<?> type, final Object value) {
        this.type = type;
        this.value = value;
    }

    public Object getValue() {
        return value;
    }

    public Class<?> getType() {
        return type;
    }

    public static Function<MethodArgument, Class<?>> typeExtractor() {
        return new Function<MethodArgument, Class<?>>() {
            @Override
            public Class<?> apply(MethodArgument argument) {
                return argument.getType();
            }
        };
    }

    public static Function<MethodArgument, Object> valueExtractor() {
        return new Function<MethodArgument, Object>() {
            @Override
            public Object apply(MethodArgument argument) {
                return argument.getValue();
            }
        };
    }
}

Итак, я почти на месте, единственный тестовый пример здесь работает из-за кеша Java Integer, и, следовательно, экземпляр Integer одинаков во всей цепочке вызовов ... Это, однако, не работает с пользовательскими объектами, я всегда в конечном итоге с InvocationTargetException (причина: «Аргумент (ы) разные!») ...

Типы правильные, но переданные экземпляры не идентичны установленным в методе @Parameters.

Есть идеи, как обойти это?

Ответы [ 2 ]

2 голосов
/ 01 марта 2012

Держи лошадей!

SpringSource выпекает модуль spring-test-mvc: https://github.com/SpringSource/spring-test-mvc

1 голос
/ 01 марта 2012

Было бы неплохо, если бы вместо примера, который работает, вы могли бы предоставить тот, который не работает, а также предоставить трассировку стека.

Я быстро проверил Google, похоже, что Mockito плохо обрабатывает отражение от шпионских объектов .

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

Я пишу это без какой-либо IDE под рукой, поэтому могут быть ошибки компиляции, но вы поймете:

@RunWith(Parameterized.class)
public class MyControllerMappingTest {

    public interface VerifyCall<T> {
        void on(T controller);
    }

    @Parameters
    public static Collection<Object[]> requestMappings() {
        Object[][] testCases = {    
            {"GET", "/my/uri/0", new VerifyCall<MyController>() {
                @Override
                public void on(MyController controller) {
                    controller.index(0);
                }
            }}  
        };
        return asList(testCases);
    }

    private MockHttpServletRequest request;
    private MockHttpServletResponse response;
    private MyController mockedController;
    private AnnotationMethodHandlerAdapter annotationHandlerAdapter;

    private final String httpMethod;
    private final String uri;
    private final VerifyCall<MyController> verifyCall;

    public MyControllerMappingTest(String httpMethod, String uri, VerifyCall<MyController> verifyCall) {
        this.httpMethod = httpMethod;
        this.uri = uri;
        this.verifyCall = verifyCall;
    }

    @Before
    public void setup() {
        request = new MockHttpServletRequest();
        response = new MockHttpServletResponse();
        mockedController = mock(MyController.class);
        annotationHandlerAdapter = new AnnotationMethodHandlerAdapter();
    }

    @Test
    public void when_matching_mapping_constraints_then_controller_method_automatically_called() throws Exception {
        request.setMethod(httpMethod);
        request.setRequestURI(uri);
        annotationHandlerAdapter.handle(request, response, mockedController);

        verifyCall.on(verify(mockedController));
    }
}

Конечно, наличие Java Lambas поможет сделать это более читабельным.

Вы также можете использовать FunkyJFunctional :

@RunWith(Parameterized.class)
public class MyControllerMappingTest {

    @Parameters
    public static Collection<Object[]> requestMappings() {
        class IndexZero extends FF<MyController, Void> {{ in.index(0); }}
        Object[][] testCases = { //           
                {"GET", "/my/uri/0", withF(IndexZero.clas)}

        };
        return asList(testCases);
    }

    private MockHttpServletRequest request;
    private MockHttpServletResponse response;
    private MyController mockedController;
    private AnnotationMethodHandlerAdapter annotationHandlerAdapter;

    private final String httpMethod;
    private final String uri;
    private final Function<MyController, Void> verifyCall;

    public MyControllerMappingTest(String httpMethod, String uri, Function<MyController, Void> verifyCall) {
        this.httpMethod = httpMethod;
        this.uri = uri;
        this.verifyCall = verifyCall;
    }

    @Before
    public void setup() {
        request = new MockHttpServletRequest();
        response = new MockHttpServletResponse();
        mockedController = mock(MyController.class);
        annotationHandlerAdapter = new AnnotationMethodHandlerAdapter();
    }

    @Test
    public void when_matching_mapping_constraints_then_controller_method_automatically_called() throws Exception {
        request.setMethod(httpMethod);
        request.setRequestURI(uri);
        annotationHandlerAdapter.handle(request, response, mockedController);

        verifyCall.apply(verify(mockedController));
    }
}

Несколько примечаний:

  • Для удобства чтения рекомендуется ставить статические члены на первое место в классе. Методы экземпляра (setup()) также должны идти после конструктора.

  • Синтаксис массива:

Вместо этого синтаксиса:

return asList(new Object[][] {
    {},
    {}
};

Я считаю этот синтаксис более читабельным:

Object[][] testCases = {
    {},
    {}
};
return asList(testCases);
...