Mockito 3.12.4测试Mock失效致空指针及ElasticsearchClient模拟建议
问题背景
在为MessageFacade.java的delete方法编写JUnit 4.12测试用例(基于Mockito 3.12.4)时,测试执行触发NullPointerException,错误发生在schemaMetadata.getDataIndexName()调用处,错误栈如下:
SLF4J: Failed to load class "org.slf4j.impl.StaticLoggerBinder".
SLF4J: Defaulting to no-operation (NOP) logger implementation
SLF4J: See http://www.slf4j.org/codes.html#StaticLoggerBinder for further details.
Query Param SCOPE_ID=-41
Query Param ID=c17db8ea-93ee-48b5-9b5d-8d278180d2d9java.lang.NullPointerException
at org.mars.foo.service.datastore.internal.MessageFacade.delete(MessageFacade.java:242)
at org.mars.foo.service.datastore.internal.MessageFacadeTest.testBulkDelete(MessageFacadeTest.java:74)
...
同时需要获取elasticsearchClient.delete(typeDescriptor, id.toString())方法的Mock实现建议。
核心代码
MessageFacade.java的delete方法
public void delete(FooId scopeId, DataId id) throws FooIllegalArgumentException, ConfigurationException, ClientException { ArgumentValidator.notNull(scopeId, SCOPE_ID); ArgumentValidator.notNull(id, "id"); System.out.println("Query Param SCOPE_ID="+scopeId.getId().toString()); System.out.println("Query Param ID="+id.toString()); if (!isDatastoreServiceEnabled(scopeId)) { LOG.debug("Storage not enabled for account {}, return", scopeId); return; } //ORIGINAL CODE DatastoreMessage messageToBeDeleted = find(scopeId, id, DataFetchStyle.FIELDS); if (messageToBeDeleted != null) { Metadata schemaMetadata = null; try { schemaMetadata = mediator.getMetadata(scopeId, messageToBeDeleted.getTimestamp().getTime()); } catch (FooException e) { LOG.warn("Retrieving metadata error", e); } String indexName = schemaMetadata.getDataIndexName(); TypeDescriptor typeDescriptor = new TypeDescriptor(indexName, MessageSchema.MESSAGE_TYPE_NAME); ElasticsearchClient elasticsearchClient = getElasticsearchClient(); elasticsearchClient.delete(typeDescriptor, id.toString()); } else { LOG.warn("Cannot find the message to be deleted. scopeId: '{}' - id: '{}'", scopeId, id); } }
测试代码
@Test public void testBulkDelete() throws ClientException, ConfigurationException, FooIllegalArgumentException { MessageFacade messageFacade = new MessageFacade(Mockito.mock(ConfigurationProvider.class),Mockito.mock(MessageStoreMediator.class)); MessageFacade spyMessageFacade = Mockito.spy(messageFacade); DatastoreMessage datastoreMessage = new DatastoreMessageImpl(); datastoreMessage.setTimestamp(new Date()); Mockito.doReturn(datastoreMessage).when(spyMessageFacade).find(Mockito.any(),Mockito.any(),Mockito.any()); Metadata schemaMetadata = Mockito.mock(Metadata.class); Mockito.doReturn("indexName").when(schemaMetadata).getDataIndexName(); ElasticsearchClient elasticsearchClient = Mockito.mock(ElasticsearchClient.class); Mockito.doNothing().when(elasticsearchClient).delete(Mockito.any(TypeDescriptor.class), Mockito.any(String.class)); Mockito.doReturn(true).when(spyMessageFacade).isDatastoreServiceEnabled(Mockito.any()); FooId scopeId = new ScopeId("10"); DataId id = new DataIdImpl("c17db8ea-93ee-48b5-9b5d-8d278180d2d9"); spyMessageFacade.delete(scopeId, id); }
解决方案
1. 解决NullPointerException问题
NPE的根源是:你创建了Metadata的Mock对象,但未让mediator.getMetadata()返回该Mock实例。业务代码中schemaMetadata来自mediator.getMetadata()调用,由于未对该方法Mock,它返回null,最终调用schemaMetadata.getDataIndexName()时触发空指针。
修改测试代码,添加对mediator.getMetadata()的Mock:
// 单独创建mediator的Mock实例,方便后续配置 MessageStoreMediator mediator = Mockito.mock(MessageStoreMediator.class); MessageFacade messageFacade = new MessageFacade(Mockito.mock(ConfigurationProvider.class), mediator); MessageFacade spyMessageFacade = Mockito.spy(messageFacade); // ... 其他测试代码保持不变 // 让mediator.getMetadata()返回你创建的schemaMetadata Mock Mockito.doReturn(schemaMetadata).when(mediator).getMetadata(Mockito.any(FooId.class), Mockito.any(Long.class));
同时建议在业务代码中添加空值判断,从源头避免类似NPE:
// 在调用schemaMetadata方法前先判空 if (schemaMetadata != null) { String indexName = schemaMetadata.getDataIndexName(); TypeDescriptor typeDescriptor = new TypeDescriptor(indexName, MessageSchema.MESSAGE_TYPE_NAME); ElasticsearchClient elasticsearchClient = getElasticsearchClient(); elasticsearchClient.delete(typeDescriptor, id.toString()); } else { LOG.warn("Failed to retrieve metadata, skip delete operation"); }
2. ElasticsearchClient.delete方法的Mock实现建议
当前基础Mock(Mockito.doNothing())已可用,可进一步优化测试覆盖度:
- 验证方法调用有效性:确保delete方法被正确调用,参数符合预期
// 执行delete操作后添加验证逻辑 Mockito.verify(elasticsearchClient, Mockito.times(1)) .delete(Mockito.argThat(typeDescriptor -> typeDescriptor.getIndexName().equals("indexName") && typeDescriptor.getTypeName().equals(MessageSchema.MESSAGE_TYPE_NAME)), Mockito.eq(id.toString()));
- Mock异常场景:测试delete方法抛出异常时的业务处理逻辑(若业务代码有异常处理逻辑)
// Mock delete方法抛出ClientException Mockito.doThrow(new ClientException("Delete failed")).when(elasticsearchClient) .delete(Mockito.any(TypeDescriptor.class), Mockito.any(String.class)); // 执行delete后,验证日志或异常处理逻辑(如捕获异常、记录告警等)
- 绑定ElasticsearchClient Mock实例:若
getElasticsearchClient()是MessageFacade的方法,需Mock该方法使其返回你创建的elasticsearchClient实例,否则业务代码会使用真实客户端而非Mock:
Mockito.doReturn(elasticsearchClient).when(spyMessageFacade).getElasticsearchClient();
内容的提问来源于stack exchange,提问作者SSM Tariq

