如何为基于MongoDB的Jobrunr存储配置编写单元测试
解决Jobrunr AmazonDocumentDBStorageProvider配置类单元测试Mock问题
问题背景
现有Spring配置类通过AmazonDocumentDBStorageProvider为Jobrunr提供存储支持,依赖MongoDB。编写单元测试以满足Sonar流水线95%覆盖率要求时,Mock MongoDB相关组件遇到空指针错误。
配置类代码
import com.mongodb.ConnectionString; import com.mongodb.MongoClientSettings; import com.mongodb.client.MongoClient; import com.mongodb.client.MongoClients; import org.bson.UuidRepresentation; import org.jobrunr.storage.StorageProvider; import org.jobrunr.storage.nosql.documentdb.AmazonDocumentDBStorageProvider; import org.springframework.beans.factory.annotation.Value; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; @Configuration public class BeanConfig { @Value("${mongodb.host}") private String mongoHost; @Value("${mongodb.username}") private String username; @Value("${mongodb.password}") private String password; @Bean public StorageProvider getStorageProvider() { return getAmazonDocumentDB(); } private AmazonDocumentDBStorageProvider getAmazonDocumentDB() { ConnectionString connectionString = new ConnectionString(getAmazonDocumentDbConnectionString()); final MongoClientSettings mongoClientSettings = MongoClientSettings.builder() .applyConnectionString(connectionString) .uuidRepresentation(UuidRepresentation.JAVA_LEGACY) .build(); final MongoClient mongoClient = MongoClients.create(mongoClientSettings); return new AmazonDocumentDBStorageProvider(mongoClient); } private String getAmazonDocumentDbConnectionString() { final String template = "mongodb://%s:%s@%s/?replicaSet=rs0&readpreference=%s&retryWrites=false"; final String username = this.username; final String password = this.password; final String host = this.mongoHost; final String readPreference = "secondaryPreferred"; return String.format(template, username, password, host, readPreference); } }
现有单元测试代码
@ExtendWith({MockitoExtension.class}) public class BeanConfigTest { @InjectMocks private BeanConfig beanConfig; @Value("${mongodb.host}") private String mongoHost = "localhost:27017"; @Value("${mongodb.username}") private String username = "user"; @Value("${mongodb.password}") private String password = "pass"; private static MockedStatic<MongoClientSettings> mongoClientSettings = Mockito.mockStatic(MongoClientSettings.class); private static MockedStatic<MongoClients> mongoClients = Mockito.mockStatic(MongoClients.class); private static MongoClient mockMongoClient = Mockito.mock(MongoClient.class); @InjectMocks private ActionHandler actionHandler; @Mock private StorageService storageService; @BeforeAll public static void init(){ mongoClients.when(() -> MongoClients.create(any(MongoClientSettings.class))).thenReturn(mockMongoClient); } @Test public void testActionHandler() { StorageProvider storageProvider = beanConfig.getStorageProvider(); assertNotNull(storageProvider); } }
报错信息
java.lang.NullPointerException: Cannot invoke "com.mongodb.client.MongoDatabase.getCollection(String)" because "this.jobrunrDatabase" is null at org.jobrunr.storage.nosql.mongo.MongoDBCreator.<init>(MongoDBCreator.java:49) at org.jobrunr.storage.nosql.mongo.MongoDBCreator.<init>(MongoDBCreator.java:42) at org.jobrunr.storage.nosql.documentdb.AmazonDocumentDBStorageProvider.runMigrations(AmazonDocumentDBStorageProvider.java:59) at org.jobrunr.storage.nosql.mongo.MongoDBStorageProvider.setUpStorageProvider(MongoDBStorageProvider.java:145) at org.jobrunr.storage.nosql.mongo.MongoDBStorageProvider.<init>(MongoDBStorageProvider.java:126) at org.jobrunr.storage.nosql.mongo.MongoDBStorageProvider.<init>(MongoDBStorageProvider.java:111) at org.jobrunr.storage.nosql.mongo.MongoDBStorageProvider.<init>(MongoDBStorageProvider.java:91) at org.jobrunr.storage.nosql.documentdb.AmazonDocumentDBStorageProvider.<init>(AmazonDocumentDBStorageProvider.java:26) at com.cs.config.BeanConfig.getAmazonDocumentDB(BeanConfig.java:39) at com.cs.config.BeanConfig.getStorageProvider(BeanConfig.java:28) at com.cs.config.BeanConfigTest.testActionHandler(BeanConfigTest.java:87) at java.base/java.lang.reflect.Method.invoke(Method.java:568) at java.base/java.util.ArrayList.forEach(ArrayList.java:1511) at java.base/java.util.ArrayList.forEach(ArrayList.java:1511)
解决方案
问题分析
- @Value注入未生效:MockitoExtension不会自动处理
@Value注解,导致BeanConfig的私有字段为null,生成连接字符串时会出错。 - MongoClient Mock不完整:
AmazonDocumentDBStorageProvider初始化时会调用MongoClient.getDatabase()获取Jobrunr数据库,未Mock该方法导致空指针。 - 不必要的静态Mock:
MongoClientSettings不需要MockStatic,正常创建实例即可。
修正后的测试代码
import org.junit.jupiter.api.AfterAll; import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; import org.mockito.Mock; import org.mockito.MockedStatic; import org.mockito.Mockito; import org.mockito.junit.jupiter.MockitoExtension; import org.springframework.test.util.ReflectionTestUtils; import com.mongodb.MongoClientSettings; import com.mongodb.client.MongoClient; import com.mongodb.client.MongoClients; import com.mongodb.client.MongoCollection; import com.mongodb.client.MongoDatabase; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertNotNull; import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.eq; @ExtendWith(MockitoExtension.class) public class BeanConfigTest { @InjectMocks private BeanConfig beanConfig; @Mock private MongoClient mockMongoClient; @Mock private MongoDatabase mockMongoDatabase; @Mock private MongoCollection mockMongoCollection; private static MockedStatic<MongoClients> mongoClientsStatic; @BeforeAll public static void initStaticMocks() { mongoClientsStatic = Mockito.mockStatic(MongoClients.class); } @AfterAll public static void closeStaticMocks() { mongoClientsStatic.close(); } @BeforeEach public void setUp() { // 手动设置BeanConfig的@Value私有字段 ReflectionTestUtils.setField(beanConfig, "mongoHost", "localhost:27017"); ReflectionTestUtils.setField(beanConfig, "username", "user"); ReflectionTestUtils.setField(beanConfig, "password", "pass"); // 配置MongoClient完整调用链的Mock行为 mongoClientsStatic.when(() -> MongoClients.create(any(MongoClientSettings.class))).thenReturn(mockMongoClient); Mockito.when(mockMongoClient.getDatabase(eq("jobrunr"))).thenReturn(mockMongoDatabase); Mockito.when(mockMongoDatabase.getCollection(any(String.class))).thenReturn(mockMongoCollection); } @Test public void testGetStorageProvider() { StorageProvider storageProvider = beanConfig.getStorageProvider(); assertNotNull(storageProvider); } @Test public void testGetAmazonDocumentDbConnectionString() { // 调用私有方法验证连接字符串生成逻辑 String connectionString = (String) ReflectionTestUtils.invokeMethod(beanConfig, "getAmazonDocumentDbConnectionString"); assertNotNull(connectionString); String expected = "mongodb://user:pass@localhost:27017/?replicaSet=rs0&readpreference=secondaryPreferred&retryWrites=false"; assertEquals(expected, connectionString); } }
关键改进点
- 使用
ReflectionTestUtils手动设置BeanConfig的私有字段,解决@Value注入不生效的问题。 - 完整Mock MongoClient的调用链:
MongoClients.create()→MongoClient.getDatabase()→MongoDatabase.getCollection(),覆盖Jobrunr初始化时的数据库操作,避免空指针。 - 静态Mock资源在
@BeforeAll初始化,@AfterAll销毁,防止测试资源泄漏。 - 新增对私有方法
getAmazonDocumentDbConnectionString()的测试,提升代码覆盖率。
内容的提问来源于stack exchange,提问作者Sachin
相关产品推荐
相关产品推荐

