Skip to content

Commit

Permalink
Add parameter support for @Captor with JUnit Jupiter (#3133)
Browse files Browse the repository at this point in the history
Fixes #1382

Co-authored-by: yevhen lazhyntsev <yevhen.lazhyntsev@hulu.com>
  • Loading branch information
EugenLaz and yevhen lazhyntsev committed Oct 6, 2023
1 parent fb48e09 commit 6f4eb02
Show file tree
Hide file tree
Showing 8 changed files with 212 additions and 23 deletions.
2 changes: 1 addition & 1 deletion src/main/java/org/mockito/Captor.java
Expand Up @@ -46,6 +46,6 @@
* @since 1.8.3
*/
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.FIELD)
@Target({ElementType.FIELD, ElementType.PARAMETER})
@Documented
public @interface Captor {}
Expand Up @@ -5,6 +5,7 @@
package org.mockito.internal.configuration;

import java.lang.reflect.Field;
import java.lang.reflect.Parameter;

import org.mockito.ArgumentCaptor;
import org.mockito.Captor;
Expand All @@ -29,4 +30,18 @@ public Object process(Captor annotation, Field field) {
Class<?> cls = new GenericMaster().getGenericType(field);
return ArgumentCaptor.forClass(cls);
}

public static Object process(Parameter parameter) {
Class<?> type = parameter.getType();
if (!ArgumentCaptor.class.isAssignableFrom(type)) {
throw new MockitoException(
"@Captor field must be of the type ArgumentCaptor.\n"
+ "Field: '"
+ parameter.getName()
+ "' has wrong type\n"
+ "For info how to use @Captor annotations see examples in javadoc for MockitoAnnotations class.");
}
Class<?> cls = new GenericMaster().getGenericType(parameter);
return ArgumentCaptor.forClass(cls);
}
}
Expand Up @@ -5,6 +5,7 @@
package org.mockito.internal.util.reflection;

import java.lang.reflect.Field;
import java.lang.reflect.Parameter;
import java.lang.reflect.ParameterizedType;
import java.lang.reflect.Type;

Expand All @@ -17,6 +18,19 @@ public class GenericMaster {
*/
public Class<?> getGenericType(Field field) {
Type generic = field.getGenericType();
return getaClass(generic);
}

/**
* Resolves the type (parametrized type) of the parameter. If the field is not generic it returns Object.class.
*
* @param parameter the parameter to inspect
*/
public Class<?> getGenericType(Parameter parameter) {
return getaClass(parameter.getType());
}

private Class<?> getaClass(Type generic) {
if (generic instanceof ParameterizedType) {
Type actual = ((ParameterizedType) generic).getActualTypeArguments()[0];
if (actual instanceof Class) {
Expand Down
Expand Up @@ -7,7 +7,6 @@
import static org.junit.jupiter.api.extension.ExtensionContext.Namespace.create;
import static org.junit.platform.commons.support.AnnotationSupport.findAnnotation;

import java.lang.reflect.Parameter;
import java.util.HashSet;
import java.util.List;
import java.util.Optional;
Expand All @@ -20,14 +19,15 @@
import org.junit.jupiter.api.extension.ParameterContext;
import org.junit.jupiter.api.extension.ParameterResolutionException;
import org.junit.jupiter.api.extension.ParameterResolver;
import org.mockito.Mock;
import org.mockito.Mockito;
import org.mockito.MockitoSession;
import org.mockito.ScopedMock;
import org.mockito.internal.configuration.MockAnnotationProcessor;
import org.mockito.internal.configuration.plugins.Plugins;
import org.mockito.internal.session.MockitoSessionLoggerAdapter;
import org.mockito.junit.MockitoJUnitRunner;
import org.mockito.junit.jupiter.resolver.CaptorParameterResolver;
import org.mockito.junit.jupiter.resolver.CompositeParameterResolver;
import org.mockito.junit.jupiter.resolver.MockParameterResolver;
import org.mockito.quality.Strictness;

/**
Expand Down Expand Up @@ -123,6 +123,8 @@ public class MockitoExtension implements BeforeEachCallback, AfterEachCallback,

private final Strictness strictness;

private final ParameterResolver parameterResolver;

// This constructor is invoked by JUnit Jupiter via reflection or ServiceLoader
@SuppressWarnings("unused")
public MockitoExtension() {
Expand All @@ -131,6 +133,10 @@ public MockitoExtension() {

private MockitoExtension(Strictness strictness) {
this.strictness = strictness;
this.parameterResolver = new CompositeParameterResolver(
new MockParameterResolver(),
new CaptorParameterResolver()
);
}

/**
Expand Down Expand Up @@ -163,12 +169,12 @@ private Optional<MockitoSettings> retrieveAnnotationFromTestClasses(final Extens
do {
annotation = findAnnotation(currentContext.getElement(), MockitoSettings.class);

if (!currentContext.getParent().isPresent()) {
if (currentContext.getParent().isEmpty()) {
break;
}

currentContext = currentContext.getParent().get();
} while (!annotation.isPresent() && currentContext != context.getRoot());
} while (annotation.isEmpty() && currentContext != context.getRoot());

return annotation;
}
Expand All @@ -188,21 +194,16 @@ public void afterEach(ExtensionContext context) {

@Override
public boolean supportsParameter(ParameterContext parameterContext, ExtensionContext context) throws ParameterResolutionException {
return parameterContext.isAnnotated(Mock.class);
return parameterResolver.supportsParameter(parameterContext, context);
}

@Override
@SuppressWarnings("unchecked")
public Object resolveParameter(ParameterContext parameterContext, ExtensionContext context) throws ParameterResolutionException {
final Parameter parameter = parameterContext.getParameter();
Object mock = MockAnnotationProcessor.processAnnotationForMock(
parameterContext.findAnnotation(Mock.class).get(),
parameter.getType(),
parameter::getParameterizedType,
parameter.getName());
if (mock instanceof ScopedMock) {
context.getStore(MOCKITO).get(MOCKS, Set.class).add(mock);
Object resolvedParameter = parameterResolver.resolveParameter(parameterContext, context);
if (resolvedParameter instanceof ScopedMock) {
context.getStore(MOCKITO).get(MOCKS, Set.class).add(resolvedParameter);
}
return mock;
return resolvedParameter;
}
}
@@ -0,0 +1,25 @@
/*
* Copyright (c) 2018 Mockito contributors
* This program is made available under the terms of the MIT License.
*/
package org.mockito.junit.jupiter.resolver;

import org.junit.jupiter.api.extension.ExtensionContext;
import org.junit.jupiter.api.extension.ParameterContext;
import org.junit.jupiter.api.extension.ParameterResolutionException;
import org.junit.jupiter.api.extension.ParameterResolver;
import org.mockito.Captor;
import org.mockito.internal.configuration.CaptorAnnotationProcessor;

public class CaptorParameterResolver implements ParameterResolver {

@Override
public boolean supportsParameter(ParameterContext parameterContext, ExtensionContext extensionContext) throws ParameterResolutionException {
return parameterContext.isAnnotated(Captor.class);
}

@Override
public Object resolveParameter(ParameterContext parameterContext, ExtensionContext extensionContext) throws ParameterResolutionException {
return CaptorAnnotationProcessor.process(parameterContext.getParameter());
}
}
@@ -0,0 +1,43 @@
/*
* Copyright (c) 2018 Mockito contributors
* This program is made available under the terms of the MIT License.
*/
package org.mockito.junit.jupiter.resolver;

import org.junit.jupiter.api.extension.ExtensionContext;
import org.junit.jupiter.api.extension.ParameterContext;
import org.junit.jupiter.api.extension.ParameterResolutionException;
import org.junit.jupiter.api.extension.ParameterResolver;

import java.util.List;
import java.util.Optional;

public class CompositeParameterResolver implements ParameterResolver {

private final List<ParameterResolver> delegates;

public CompositeParameterResolver(final ParameterResolver... delegates) {
this.delegates = List.of(delegates);
}

@Override
public boolean supportsParameter(ParameterContext parameterContext, ExtensionContext extensionContext) throws ParameterResolutionException {
return findDelegate(parameterContext, extensionContext).isPresent();
}

@Override
@SuppressWarnings("OptionalGetWithoutIsPresent")
public Object resolveParameter(ParameterContext parameterContext, ExtensionContext extensionContext) throws ParameterResolutionException {
final ParameterResolver delegate = findDelegate(parameterContext, extensionContext).get();
return delegate.resolveParameter(parameterContext, extensionContext);
}

private Optional<ParameterResolver> findDelegate(
final ParameterContext parameterContext,
final ExtensionContext extensionContext
) {
return delegates.stream()
.filter(delegate -> delegate.supportsParameter(parameterContext, extensionContext))
.findFirst();
}
}
@@ -0,0 +1,34 @@
/*
* Copyright (c) 2018 Mockito contributors
* This program is made available under the terms of the MIT License.
*/
package org.mockito.junit.jupiter.resolver;

import org.junit.jupiter.api.extension.ExtensionContext;
import org.junit.jupiter.api.extension.ParameterContext;
import org.junit.jupiter.api.extension.ParameterResolutionException;
import org.junit.jupiter.api.extension.ParameterResolver;
import org.mockito.Mock;
import org.mockito.internal.configuration.MockAnnotationProcessor;

import java.lang.reflect.Parameter;

public class MockParameterResolver implements ParameterResolver {

@Override
public boolean supportsParameter(ParameterContext parameterContext, ExtensionContext context) throws ParameterResolutionException {
return parameterContext.isAnnotated(Mock.class);
}

@Override
@SuppressWarnings("OptionalGetWithoutIsPresent")
public Object resolveParameter(ParameterContext parameterContext, ExtensionContext context) throws ParameterResolutionException {
final Parameter parameter = parameterContext.getParameter();

return MockAnnotationProcessor.processAnnotationForMock(
parameterContext.findAnnotation(Mock.class).get(),
parameter.getType(),
parameter::getParameterizedType,
parameter.getName());
}
}

0 comments on commit 6f4eb02

Please sign in to comment.