带有 Jersey 测试和 JAX-RS 的 Mockito - UnsatisfiedDependencyException

Mockito with Jersey Test and JAX-RS - UnsatisfiedDependencyException

尝试测试一个相当简单的 JAX-RS 端点

@ApplicationScoped
@Path("mypath")
public class MyRestService {
    @Inject
    private Logger logger;

    @Inject
    private EjbService ejbService;

    @GET
    public String myMethod() {
        logger.info("...");
        return ejbService.myMethod();
    }
}

使用 Mockito 和 Jersey 测试

@RunWith(MockitoJUnitRunner.class)
public class MyRestServiceTest extends JerseyTest {
    @Mock
    private EjbService ejbService;

    @Mock
    private Logger logger;

    @InjectMocks
    private MyRestService myRestService;

    ...

    @Override
    protected Application configure() {
        MockitoAnnotations.initMocks(this);
        return new ResourceConfig().register(myRestService);
    }
}

Grizzly 容器正在为 LoggerEjbService 返回 org.glassfish.hk2.api.UnsatisfiedDependencyException,甚至认为 Mockito 正确注入了依赖项。

似乎 Grizzly 正在尝试正确地覆盖 Mockito 模拟。 如果我在 configure 方法中注册一个 AbstractBinder,一切正常。

.register(new AbstractBinder() {
    @Override
    protected void configure() {
        bind(ejbService).to(EjbService.class);
        bind(logger).to(Logger.class);
    }
});

但我觉得这不是完成注入的最佳方式。恕我直言,Mockito 风格更好。 我需要做什么来解决这个问题?

MockitoJUnitRunner 用于单元测试,JerseyTest 用于集成测试。

使用 Mockito 时,您的测试将直接调用声明的 myRestService 并且 Mockito 依赖注入将发生。

使用 JerseyTest 时,会创建一个新的 Web 容器,并且您的测试会通过 HTTP 调用与 MyRestService 通信。在这个容器中,真正的依赖注入正在发生,类 甚至没有看到你声明的模拟。

您可以像您一样一起使用 JerseyTest 和 Mockito。它只需要一些额外的配置(正如您已经发现的那样)并且不需要 @RunWith 注释。

我能够创建以下基础 class 以实现 JerseyTestMockito 之间的集成,例如 OP 旨在:

package org.itest;

import com.google.common.collect.Maps;
import org.apache.commons.lang3.reflect.FieldUtils;
import org.glassfish.jersey.internal.inject.AbstractBinder;
import org.glassfish.jersey.server.ResourceConfig;
import org.glassfish.jersey.server.ServerProperties;
import org.glassfish.jersey.test.JerseyTestNg;
import org.mockito.Mock;
import org.mockito.MockitoAnnotations;
import org.springframework.util.ReflectionUtils;

import javax.ws.rs.core.Application;
import java.lang.reflect.Field;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

/**
 * @author Nom1fan
 */
public abstract class JerseyTestBase extends JerseyTestNg.ContainerPerClassTest {

    @Override
    protected Application configure() {
        MockitoAnnotations.openMocks(this);
        ResourceConfig application = new ResourceConfig();
        Object resourceUnderTest = getResourceUnderTest();
        application.register(resourceUnderTest);

        Map<String, Object> properties = Maps.newHashMap();
        properties.put(ServerProperties.BV_SEND_ERROR_IN_RESPONSE, true);
        properties.put("contextConfigLocation", "classpath:applicationContext.xml");

// Retrieve the fields annotated on subclass as @Mock via reflection and keep each instance 
// and its type on an entry in the map, later used to bind to Jersey infra.
        HashMap<Object, Class<?>> mocksToBindMap = Maps.newHashMap();
        List<Field> fieldsWithMockAnnotation = FieldUtils.getFieldsListWithAnnotation(getClass(), Mock.class);
        for (Field declaredField : fieldsWithMockAnnotation) {
            declaredField.setAccessible(true);
            Object fieldObj = ReflectionUtils.getField(declaredField, this);
            mocksToBindMap.put(fieldObj, declaredField.getType());
        }

        application.setProperties(properties);
        application.register(new AbstractBinder() {
            @Override
            protected void configure() {
                for (Map.Entry<Object, Class<?>> mockToBind : mocksToBindMap.entrySet()) {
                    bind(mockToBind.getKey()).to(mockToBind.getValue());
                }
            }
        });
        return application;
    }

    protected abstract Object getResourceUnderTest();
}

挂钩 getResourceUnderTest 必须由扩展测试 class 实现,提供它希望测试的资源实例。

测试class例子:

import org.itest.JerseyTestBase;
import org.mockito.InjectMocks;
import org.mockito.Mock;

public class MyJerseyTest extends JerseyTestBase {
    
    @Mock
    private MockA mockA;
    @Mock
    private MockB mockB;
    @InjectMocks
    private MyResource myResource;


    @Override
    protected Object getResourceUnderTest() {
        return myResource;
    }


    @Test
    public void myTest() {
        when(mockA.foo()).thenReturn("Don't you dare go hollow");
        when(mockB.bar()).thenReturn("Praise the Sun \[T]/");

        // Test stuff
        target("url...").request()...
    }
}

MyResource class 看起来像这样:

@Path("url...")
@Controller
public class MyResource {
   
    private final MockA mockA;
    private final MockB mockB;

   @Autowired        // Mocks should get injected here
   public MyResource(MockA mockA, MockB mockB) {
      this.mockA = mockA; 
      this.mockB = mockB;
   }

   @GET
   public Response someAPI() {
     mockA.foo(); 
     mockB.bar();
   }
}

注意:我使用 Spring 和 Apache 的反射实用程序使事情变得更容易,但这不是强制性的。可以手写的简单反射代码