Spring Boot JUnit测试:不使用PowerMockito模拟System.getenv
问题
我需要为直接调用System.getenv("mongodb_url")的MyService编写JUnit测试,但测试时该方法返回null,希望在不使用PowerMockito的前提下设置环境变量以通过测试。以下是现有代码:
现有代码
Service类
@Service public class MyService{ @Autowired MongoClient mongoClient; public void save(){ String mongoUri= System.getenv("mongodb_url"); if(mongoUri!=null){ List<String> databaseNames = mongoClient.listDatabaseNames().into(new ArrayList<>()); for (String dbName : databaseNames) { log.info(dbName+"database name"); } } } }
测试类
@RunWith(SpringRunner.class) @SpringBootTest(classes = { DefaultTestService.class}) public class test { @Mock Environment environment; @InjectMocks MyService myservice; @Test public void test(){ when(System.getenv("mongodb_url")).thenReturn("mongodb://localhost:27017"); myservice.save(); } }
Gradle依赖
implementation 'junit:junit:4.13.1'
解决方案
不用PowerMockito的话,优先推荐重构代码解耦依赖的方式,也可通过反射临时修改环境变量,以下是具体可行方案:
方案一:重构Service类,依赖Spring Environment(推荐)
直接调用System.getenv会让代码耦合性高、难以测试,改为依赖Spring的Environment接口,测试时可轻松mock该依赖:
修改后的Service类
@Service public class MyService{ @Autowired MongoClient mongoClient; @Autowired Environment environment; // 注入Spring环境对象 public void save(){ // 通过Environment获取环境变量 String mongoUri= environment.getProperty("mongodb_url"); if(mongoUri!=null){ List<String> databaseNames = mongoClient.listDatabaseNames().into(new ArrayList<>()); for (String dbName : databaseNames) { log.info(dbName+"database name"); } } } }
修改后的测试类
@RunWith(SpringRunner.class) @SpringBootTest public class MyServiceTest { // 规范类名,首字母大写 @Mock MongoClient mongoClient; @Mock Environment environment; @InjectMocks MyService myService; @Test public void testSave() { // mock环境变量返回值 when(environment.getProperty("mongodb_url")).thenReturn("mongodb://localhost:27017"); // mock MongoClient的listDatabaseNames方法,避免真实调用 List<String> mockDbNames = Arrays.asList("test_db", "dev_db"); when(mongoClient.listDatabaseNames()).thenReturn(new MockMongoCursor<>(mockDbNames)); myService.save(); // 验证业务逻辑执行 verify(mongoClient, times(1)).listDatabaseNames(); } }
方案二:反射临时修改环境变量(仅适用于无法重构的场景)
通过反射修改System类中的环境变量映射,测试后需恢复原变量,避免影响其他测试用例:
测试类代码
@RunWith(SpringRunner.class) @SpringBootTest public class MyServiceTest { @Mock MongoClient mongoClient; @InjectMocks MyService myService; private String originalMongoUrl; @Before public void setup() throws Exception { // 保存原环境变量值 originalMongoUrl = System.getenv("mongodb_url"); // 设置测试用的环境变量 setEnvironmentVariable("mongodb_url", "mongodb://localhost:27017"); } @After public void teardown() throws Exception { // 恢复原环境变量状态 if (originalMongoUrl == null) { removeEnvironmentVariable("mongodb_url"); } else { setEnvironmentVariable("mongodb_url", originalMongoUrl); } } @Test public void testSave() { List<String> mockDbNames = Arrays.asList("test_db"); when(mongoClient.listDatabaseNames()).thenReturn(new MockMongoCursor<>(mockDbNames)); myService.save(); verify(mongoClient, times(1)).listDatabaseNames(); } // 反射设置环境变量的工具方法 private void setEnvironmentVariable(String key, String value) throws Exception { Class<?> systemClass = Class.forName("java.lang.System"); Field envField = systemClass.getDeclaredField("env"); envField.setAccessible(true); Map<String, String> env = (Map<String, String>) envField.get(null); env.put(key, value); // 更新系统缓存,保证读取一致性 Field unsafeField = systemClass.getDeclaredField("theUnsafe"); unsafeField.setAccessible(true); Object unsafe = unsafeField.get(null); Method putVolatileMethod = unsafe.getClass().getMethod("putObjectVolatile", Object.class, long.class, Object.class); Field offsetField = systemClass.getDeclaredField("envOffset"); offsetField.setAccessible(true); putVolatileMethod.invoke(unsafe, systemClass, offsetField.getLong(null), env); } // 反射移除环境变量的工具方法 private void removeEnvironmentVariable(String key) throws Exception { Class<?> systemClass = Class.forName("java.lang.System"); Field envField = systemClass.getDeclaredField("env"); envField.setAccessible(true); Map<String, String> env = (Map<String, String>) envField.get(null); env.remove(key); // 更新系统缓存 Field unsafeField = systemClass.getDeclaredField("theUnsafe"); unsafeField.setAccessible(true); Object unsafe = unsafeField.get(null); Method putVolatileMethod = unsafe.getClass().getMethod("putObjectVolatile", Object.class, long.class, Object.class); Field offsetField = systemClass.getDeclaredField("envOffset"); offsetField.setAccessible(true); putVolatileMethod.invoke(unsafe, systemClass, offsetField.getLong(null), env); } }
方案三:使用Spring Boot的@TestPropertySource(Spring环境专属)
如果测试基于Spring Boot环境,可通过@TestPropertySource设置属性,同时需要修改Service类从Spring属性取值:
修改后的Service类
@Service public class MyService{ @Autowired MongoClient mongoClient; @Value("${mongodb_url}") // 直接注入属性值 private String mongoUri; public void save(){ if(mongoUri!=null){ List<String> databaseNames = mongoClient.listDatabaseNames().into(new ArrayList<>()); for (String dbName : databaseNames) { log.info(dbName+"database name"); } } } }
测试类
@RunWith(SpringRunner.class) @SpringBootTest @TestPropertySource(properties = {"mongodb_url=mongodb://localhost:27017"}) public class MyServiceTest { @Mock MongoClient mongoClient; @InjectMocks MyService myService; @Test public void testSave() { List<String> mockDbNames = Arrays.asList("test_db"); when(mongoClient.listDatabaseNames()).thenReturn(new MockMongoCursor<>(mockDbNames)); myService.save(); verify(mongoClient).listDatabaseNames(); } }
补充:完善Gradle依赖
原依赖缺少Spring Boot测试和Mockito相关依赖,需补充:
dependencies { implementation 'junit:junit:4.13.1' testImplementation 'org.springframework.boot:spring-boot-starter-test' testImplementation 'org.mockito:mockito-core:3.12.4' }
内容的提问来源于stack exchange,提问作者Vijay
相关产品推荐
相关产品推荐

