跳转到主内容
极星编程网:以代码为星,赴技术山海!

JUnit测试中覆盖异常捕获块(Catch Block)的实用策略

本文深入探讨了在junit测试中如何有效覆盖 java 代码中的异常捕获块(catch block),特别是当异常由内部依赖抛出时。我们将详细解释为何直接模拟服务方法抛出异常的尝试会失败,并提供一个基于mockito模拟内部依赖抛出特定检查型异常的正确方法,以确保异常处理逻辑得到充分测试,并最终抛出预期的自定义运行时异常。 在软件开发中,健壮的异常处理是构建可靠系统的关键一环。为了确保我们的异常捕获逻辑能够按预期工作,编写相应的单元测试变得至关重要。然而,测试那些由内部依赖抛出并由当前方法捕获的异常,常常会给开发者带来困扰。本文将详细阐述如何利用Mockito框架,精准地模拟这些场景,从而有效覆盖代码中的异常捕获块。 理解测试异常捕获块的挑战 考虑以下服务方法,它调用一个外部生产者(snsProducer)发送消息,并捕获可能发生的JsonProcessingException,然后将其包装成一个自定义的SnSException抛出:
public class MyService { private final SnsProducer snsProducer; public MyService(SnsProducer snsProducer) { this.snsProducer = snsProducer; } public void doCreate(String message) { try { snsProducer.send(message); } catch (JsonProcessingException jpe) { // 捕获JsonProcessingException并抛出自定义的SnSException throw new SnSException("Could not parse Message to publish to SNS", jpe); } } } // SnsProducer 接口示例 public interface SnsProducer { void send(String message) throws JsonProcessingException; } // 自定义运行时异常 SnSException public class SnSException extends RuntimeException { public SnSException(String message, Throwable cause) { super(message, cause); } }
我们的目标是测试当snsProducer.send(message)抛出JsonProcessingException时,doCreate方法是否能正确捕获并抛出SnSException。 常见的错误尝试及原因分析 许多开发者在尝试测试上述场景时,可能会首先想到直接模拟MyService的doCreate方法来抛出异常,如下所示:
@Test void snsTest_incorrectAttempt1() { // 假设service是MyService的实例,这里尝试模拟service的doCreate方法 // 当doCreate方法被调用时,直接抛出JsonProcessingException // 注意:doCreate方法签名中并未声明抛出JsonProcessingException when(service.doCreate(anyString())).thenThrow(new JsonProcessingException("Json Processing Error"){}); // 期望doCreate方法会抛出SnSException assertThrows(SnSException.class, () -> service.doCreate(anyString())); }
错误原因分析: 这种尝试会导致 编译错误 或运行时异常,例如Checked exception is invalid for this method!。这是因为MyService.doCreate方法的签名并没有声明它会抛出JsonProcessingException(它内部捕获了该异常)。Mockito的when().thenThrow()方法要求模拟抛出的检查型异常必须与被模拟方法的签名兼容。由于doCreate方法本身不抛出JsonProcessingException,所以直接模拟它抛出这个异常是不合法的。 另一种尝试可能是: Eclipse导入Android或其他的JAVA项目的正确方法 WORD版 本文档主要讲述的是Eclipse导入Android或其他的JAVA项目的正确方法;希望本文档会给有需要的朋友带来帮助;感兴趣的朋友可以过来看看 下载
@Test void snsTest_incorrectAttempt2() { // 假设service是MyService的实例,这里尝试模拟service的doCreate方法 // 这种模拟方式本身是合法的,但它绕过了doCreate方法内部的try-catch逻辑 when(service.doCreate(anyString())).thenThrow(new SnSException("Exception")); // 期望doCreate方法会抛出SnSException assertThrows(SnSException.class, () -> service.doCreate(anyString())); }
错误原因分析: 尽管这种模拟在语法上是合法的(因为SnSException是运行时异常,或者如果doCreate方法声明了SnSException),但它并没有真正测试到doCreate方法内部的try-catch逻辑。这种模拟方式完全跳过了snsProducer.send()的调用,直接让doCreate方法抛出异常。这并不是我们想要测试的场景——我们想测试的是snsProducer抛出异常后,doCreate如何响应。因此,如果你的测试期望的是snsProducer抛出JsonProcessingException后,doCreate方法内部的catch块被执行并抛出SnSException,那么这种模拟方式将无法覆盖该逻辑。 正确的测试策略:模拟内部依赖 正确的做法是模拟MyService所依赖的snsProducer对象,让它在被调用时抛出JsonProcessingException。这样,MyService.doCreate方法内部的try-catch块就会被触发。 以下是使用JUnit 5和Mockito的完整测试示例:
import com.fasterxml.jackson.core.JsonProcessingException; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; import org.mockito.InjectMocks; import org.mockito.Mock; import org.mockito.junit.jupiter.MockitoExtension; import static org.junit.jupiter.api.Assertions.assertThrows; import static org.mockito.Mockito.*; // 假设这些是你的实际类和接口 // public interface SnsProducer { void send(String message) throws JsonProcessingException; } // public class MyService { ... } // public class SnSException extends RuntimeException { ... } @ExtendWith(MockitoExtension.class) // 启用Mockito JUnit 5扩展 class MyServiceTest { @Mock // 模拟SnsProducer接口 private SnsProducer snsProducer; @InjectMocks // 注入模拟的SnsProducer到MyService实例中 private MyService myService; // 可以选择在每个测试前初始化,但@Mock和@InjectMocks通常会自动处理 // @BeforeEach // void setUp() { // // 如果不使用@ExtendWith(MockitoExtension.class),则需要手动初始化 // // MockitoAnnotations.openMocks(this); // // myService = new MyService(snsProducer); // 如果没有@InjectMocks // } @Test void doCreate_shouldThrowSnSException_whenJsonProcessingExceptionOccurs() throws JsonProcessingException { String testMessage = "{\"key\":\"value\"}"; // 1. 模拟内部依赖 snsProducer 的行为 // 当 snsProducer.send(testMessage) 被调用时,抛出 JsonProcessingException // 注意:这里使用的是 doThrow().when() 语法,因为它更适合模拟 void 方法抛出检查型异常 doThrow(new JsonProcessingException("Simulated JSON processing error") {}) .when(snsProducer).send(testMessage); // 2. 调用被测试的服务方法 // 期望 myService.doCreate(testMessage) 会抛出 SnSException assertThrows(SnSException.class, () -> myService.doCreate(testMessage)); // 3. 验证 snsProducer.send 方法确实被调用了一次 verify(snsProducer, times(1)).send(testMessage); } @Test void doCreate_shouldCallSnsProducerSend_whenMessageIsValid() throws JsonProcessingException { String testMessage = "{\"key\":\"value\"}"; // 1. 模拟 snsProducer 的正常行为(不抛出异常) doNothing().when(snsProducer).send(testMessage); // 2. 调用服务方法 myService.doCreate(testMessage); // 3. 验证 snsProducer.send 方法确实被调用了一次 verify(snsProducer, times(1)).send(testMessage); } }
代码解释: @Mock private SnsProducer snsProducer; : 声明一个SnsProducer的模拟对象。 @InjectMocks private MyService myService; : 创建MyService的一个实例,并将所有被@Mock标记的依赖(这里是snsProducer)自动注入到myService中。 doThrow(new JsonProcessingException("Simulated JSON processing error") {}).when(snsProducer).send(testMessage); : 这是核心步骤。我们指示Mockito,当snsProducer对象的send方法被调用并传入testMessage时,它应该抛出一个JsonProcessingException。 JsonProcessingException("Simulated JSON processing error") {} : 创建一个匿名内部类实例,以避免直接实例化抽象类。 doThrow().when() : 这种语法常用于模拟void方法抛出异常,或者当when().thenThrow()语法因为类型擦除或其他原因导致问题时。对于非void方法,when(snsProducer.send(testMessage)).thenThrow(...) 同样有效。 assertThrows(SnSException.class, () -> myService.doCreate(testMessage)); : 这是JUnit 5提供的断言方法,用于验证在执行lambda表达式myService.doCreate(testMessage)时,是否会抛出SnSException类型的异常。 verify(snsProducer, times(1)).send(testMessage); : 这是一个可选但推荐的步骤,用于验证snsProducer.send方法确实被调用了一次。这有助于确认测试路径是正确的,并且服务方法确实尝试了与依赖进行交互。 总结与最佳实践 模拟异常的来源: 始终模拟抛出异常的 实际来源 (即内部依赖),而不是被测试的服务方法本身。 理解方法签名: 在使用when().thenThrow()时,要确保模拟抛出的检查型异常与被模拟方法的签名兼容。对于void方法或需要更灵活控制的场景,doThrow().when()通常是更好的选择。 使用assertThrows: JUnit 5的assertThrows方法是验证异常抛出的标准和推荐方式,它简洁且易读。 验证交互: 使用Mockito.verify()来验证模拟对象的方法是否被按预期调用,这能增强测试的健壮性。 覆盖所有路径: 除了异常路径,也要确保测试正常执行路径,以确保服务方法在没有异常发生时也能正确工作。 通过遵循这些策略,您可以有效地为Java代码中的异常捕获块编写单元测试,从而提高代码的质量和可靠性。

相关文章