【问题标题】:Mockito: How to mock spring special DI that the injected object doesn't have no-arg constructorMockito:如何模拟注入对象没有无参数构造函数的弹簧特殊DI
【发布时间】:2021-04-01 02:09:19
【问题描述】:

我在单元测试中使用Mockito 3.4.6,实际上,我已将 Mockito 集成到我的单元测试中并且效果很好。虽然,现在我需要优化一些单元测试,这是一个 特殊的依赖注入,注入的对象没有无参数构造函数,我尝试了@Spy 但它没用。

我的测试:我试过 1.@Spy; 2. @Spy 使用= getDtInsightApi() 设置实例; 3.@Spy@InjectMocks,所有的测试都失败了。正如 Mockito 文档所说,它似乎不适用于这种情况。

@InjectMocks Mockito 将尝试仅通过构造函数注入来注入模拟, setter 注入或属性注入,如下所述。

如果只使用@Spy,也会抛出MockitoException

org.mockito.exceptions.base.MockitoException: 
Failed to release mocks

This should not happen unless you are using a third-part mock maker

...
Caused by: org.mockito.exceptions.base.MockitoException: Unable to initialize @Spy annotated field 'api'.
Please ensure that the type 'DtInsightApi' has a no-arg constructor.
...
Caused by: org.mockito.exceptions.base.MockitoException: Please ensure that the type 'DtInsightApi' has a no-arg constructor.

查看我的伪代码如下

配置类:

@Configuration
public class SdkConfig {

    @Resource
    private EnvironmentContext environmentContext;

    @Bean(name = "api")
    public DtInsightApi getApi() {
     
        DtInsightApi.ApiBuilder builder = new DtInsightApi.ApiBuilder()
                    .setServerUrls("sdkUrls")
        return builder.buildApi();
    }
}

DtInsightApi class 没有公共的无参数构造函数并通过其内部类获取实例

public class DtInsightApi {
    private String[] serverUrls;

    DtInsightApi(String[] serverUrls) {
        this.serverUrls = serverUrls;
    }
    
    // inner class
    public static class ApiBuilder {
        String[] serverUrls;

        public ApiBuilder() {
        }
        ...code...

        public DtInsightApi buildApi() {
           return new DtInsightApi(this.serverUrls);
        }
    }

    ...code...

}

单元测试类:

public Test{
   
   @Autowired
   private PendingTestService service;

   @Spy
   private Api api = getDtInsightApi();

   @Mock
   private MockService mockService;

   @Before
    public void setUp() throws Exception {
        // open mock
        MockitoAnnotations.openMocks(this);
        // i use doReturn(...).when() for @Spy object
        Mockito.doReturn(mockService).when(api)
                   .getSlbApiClient(MockService.class);
        Mockito.when(mockService.addOrUpdate(any(MockDTO.class)))
                   .thenReturn(BaseObject.getApiResponseWithSuccess());
    }

    public DtInsightApi getDtInsightApi () {
        return new DtInsightApi.ApiBuilder()
                .setServerUrls(new String[]{"localhost:8080"})
                .buildApi();
    }

    @Test
    public void testUpdate() {
        service.update();
    }
}

PendingTestService:

@Service
public class PendingTestService{
   
   @Autowired
   DtInsightApi api;

   public void update() {
      // here mockService isn't the object i mocked
      MockService mockService = api.getSlbApiClient(MockService.class);
      mockService.update();
   }
}

问题:如何模拟没有no-arg构造函数的DI对象DtInsightApi。

【问题讨论】:

  • 我相信要走的路是Mockito.mock(DtInsightApi.class)。然后你存根所有调用的方法。
  • 您可以尝试模拟构建器类和相关方法。因此,有了这个,您可以很好地模拟构建器的build 方法并返回一个纯粹模拟的DtInsightApi 实例。顺便说一句,您提到您尝试对此进行监视,但没有成功。您注意到什么问题?
  • 嗨akortex91,我调试了MockService mockService = api.getSlbApiClient(MockService.class);,发现mockService实例不是mockito代理对象。
  • @naimdjon,在DtInsightApi api = Mockito.mock(DtInsightApi.class); 之后我应该怎么做才能将它注入弹簧?
  • @KDFinal 您必须使用模拟实例化服务或创建具有@Bean 注释的测试配置,该注释提供DtInsightApi 的实例。

标签: java junit mockito


【解决方案1】:

在查看 Spring 文档关于单元测试后,我找到了使用 @MockBean 的解决方案。

Spirng 文档:https://docs.spring.io/spring-boot/docs/1.5.2.RELEASE/reference/html/boot-features-testing.html

根据 Spring 文档,您可以使用 @MockBean 模拟 ApplicationContext 中的 bean,因此我可以使用 @MockBean 模拟 DtInsightApi

在运行测试时,有时需要在应用程序上下文中模拟某些组件。例如, 你可能有一些在开发过程中不可用的远程服务的外观。模拟在以下情况下也很有用 您想模拟在真实环境中可能难以触发的故障。

Spring Boot 包含一个 @MockBean 注释,可用于为 ApplicationContext 中的 bean 定义 Mockito 模拟。 您可以使用注解来添加新的 bean,或者替换单个现有的 bean 定义。注释可以直接在测试类上使用, 在测试中的字段上,或在@Configuration 类和字段上。在字段上使用时,创建的模拟实例也将被注入。 模拟 bean 在每个测试方法之后都会自动重置。

我的解决方案:使用@MockBeanBDDMockito.given(...).willReturn(...),使用 @Qualifier("api") 指定 bean 名称,因为 @MockBean 是按类类型注入的,如果您有相同的类 bean,则需要指定 bean 名称。

我在测试类中的代码:

public class Test{
    @MockBean
    @Qualifier("api")
    private DtInsightApi api;

   @Mock
   private MockService mockService;

    @Before
    public void setUp() throws Exception {
        // open mock
        MockitoAnnotations.openMocks(this);
        BDDMockito.given(this.api.getSlbApiClient(MockService.class)).willReturn(mockService);
    }

    @Autowired
    private PendingTestService service;


    @Test
    public void testUpdate() {
        service.update();
    }
}

调试mockService可以看到mockService实例是由Mockito生成的,mock成功。

您也可以参考 Spring docs 示例: 在单元测试中模拟 RemoteService

import org.junit.*;
import org.junit.runner.*;
import org.springframework.beans.factory.annotation.*;
import org.springframework.boot.test.context.*;
import org.springframework.boot.test.mock.mockito.*;
import org.springframework.test.context.junit4.*;

import static org.assertj.core.api.Assertions.*;
import static org.mockito.BDDMockito.*;

@RunWith(SpringRunner.class)
@SpringBootTest
public class MyTests {

    @MockBean
    private RemoteService remoteService;

    @Autowired
    private Reverser reverser;

    @Test
    public void exampleTest() {
        // RemoteService has been injected into the reverser bean
        given(this.remoteService.someCall()).willReturn("mock");
        String reverse = reverser.reverseSomeCall();
        assertThat(reverse).isEqualTo("kcom");
    }

}

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 2023-04-07
    • 2016-11-19
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多