如何测试内部捕获的异常

如何测试内部捕获的异常

本文深入探讨了在单元测试中如何处理和验证被内部捕获的异常。当方法内部抛出异常但随即被 `try-catch` 块捕获并处理(例如仅记录日志)时,传统的 `assertThrows` 机制将无法直接验证。文章分析了这种设计模式带来的测试挑战,并提供了两种主要解决方案:首先是推荐通过重构代码以提高可测试性,例如使用 `Optional` 或自定义结果对象来明确指示操作结果;其次是针对无法立即重构的现有代码,探讨了通过验证日志输出或利用 `fail()` 方法来间接测试异常处理逻辑的策略。

理解问题:内部捕获异常的测试挑战

软件开发中,我们经常会遇到方法内部抛出异常,但这些异常被随后的 try-catch 块捕获并处理的情况。例如,异常可能被记录下来,但不会重新抛出或以其他方式向上层调用者传递。这种设计模式给单元测试带来了挑战,因为标准的 assertThrows 断言机制只能检测到从被测试方法中 抛出 的异常,而无法感知在方法内部被捕获的异常。

考虑以下示例代码:

Class A (调用者)

public class A {    private static Logger logger = LoggerFactory.getLogger("A");    private B b;    public A() {        b = new B();    }    public void methodA() {        b.methodB();        logger.info("A");    }}

Class B (被调用者,内部抛出并捕获异常)

import org.slf4j.Logger;import org.slf4j.LoggerFactory;public class B {    private static Logger logger = LoggerFactory.getLogger("B");    public B() {        // 通常logger在构造函数中初始化一次    }    public void methodB() {        try {            // 模拟一个内部异常            throw new Exception("NULL");        } catch(Exception e) {            // 异常被捕获并记录,但未重新抛出            logger.info("Exception thrown internally in B", e);        }    }}

当尝试使用 assertThrows 来测试 Class B 内部抛出的异常时,会遇到问题:

错误的测试示例

import org.junit.jupiter.api.Test;import static org.junit.jupiter.api.Assertions.assertThrows;public class ATest { // 假设这是测试 A 或 B 的类    @Test    public void testExceptionInB() {        B b = new B();        // 尝试断言 b.methodB() 抛出异常,但它内部已经捕获了        assertThrows(Exception.class, () -> b.methodB());    }}

执行上述测试会得到如下错误:

Expected java.lang.Exception to be thrown, but nothing was thrown.org.opentest4j.AssertionFailedError: Expected java.lang.Exception to be thrown, but nothing was thrown.

这正是因为 methodB 中的 catch 块已经处理了异常,使得 methodB 方法本身正常完成,没有向外抛出任何异常。

核心问题:异常处理设计缺陷

上述 Class B 的设计模式,即在内部捕获并“吞噬”异常而不向调用者提供任何明确反馈,通常被认为是一种不良实践。这种设计存在以下几个问题:

可测试性差:如上所示,难以直接验证内部异常的发生。错误信息丢失:调用者无法得知操作是否失败以及失败的具体原因。调试困难:当系统行为异常时,由于错误信息被内部消化,定位问题变得更加复杂。违反“命令-查询分离”原则:一个方法在执行某个命令时,应该通过返回值或异常明确其结果。

理想情况下,异常应该在能够处理它们的层次被捕获。如果一个方法无法完全处理异常,它应该重新抛出(可能封装为更具体的业务异常),或者通过返回值明确指示失败状态。

推荐方案:重构以提高可测试性

解决内部捕获异常测试问题的最佳方法是重构代码,使其遵循更好的异常处理实践,从而自然地提高可测试性。

方案一:返回 Optional 或自定义结果对象

如果一个操作可能失败但又不想强制调用者处理异常(例如,操作失败不认为是“异常”情况,而是一种预期结果),可以使用 Optional 或自定义结果对象来明确表示操作的成功或失败,并携带相关信息。

企业网站通用源码1.0 企业网站通用源码1.0

企业网站通用源码是以aspcms作为核心进行开发的asp企业网站源码。企业网站通用源码是一套界面设计非常漂亮的企业网站源码,是2016年下半年的又一力作,适合大部分的企业在制作网站是参考或使用,源码亲测完整可用,没有任何功能限制,程序内核使用的是aspcms,如果有不懂的地方或者有不会用的地方可以搜索aspcms的相关技术问题来解决。网站UI虽然不是特别细腻,但是网站整体格调非常立体,尤其是通观全

企业网站通用源码1.0 0 查看详情 企业网站通用源码1.0

Class B 重构示例 (使用 Optional)

import org.slf4j.Logger;import org.slf4j.LoggerFactory;import java.util.Optional;public class BRefactored {    private static Logger logger = LoggerFactory.getLogger("BRefactored");    public BRefactored() {        // 构造函数逻辑    }    /**     * 执行操作并返回一个Optional,指示操作是否成功。     * 如果操作失败,Optional为空,并记录错误。     * @return 如果操作成功,返回一个非空的Optional;否则返回Optional.empty()。     */    public Optional methodB() {        try {            // 模拟一个内部异常            throw new Exception("Simulated internal error");            // 假设这里是成功路径,返回一些数据            // return Optional.of("Operation successful");        } catch(Exception e) {            logger.error("Exception thrown internally in BRefactored: {}", e.getMessage());            // 明确表示操作失败            return Optional.empty();        }    }}

Class A 相应修改示例

import org.slf4j.Logger;import org.slf4j.LoggerFactory;import java.util.Optional;public class AModified {    private static Logger logger = LoggerFactory.getLogger("AModified");    private BRefactored bRefactored;    public AModified() {        bRefactored = new BRefactored();    }    public void methodA() {        Optional result = bRefactored.methodB();        if (result.isPresent()) {            logger.info("B operation successful: {}", result.get());        } else {            logger.warn("B operation failed, handling gracefully in A.");        }        logger.info("A");    }}

重构后 Class B 的测试示例现在,可以直接测试 methodB 的返回值来判断内部操作是否成功或失败:

import org.junit.jupiter.api.Test;import java.util.Optional;import static org.junit.jupiter.api.Assertions.*;public class BRefactoredTest {    @Test    void testMethodB_failureScenario() {        BRefactored b = new BRefactored();        Optional result = b.methodB();        // 断言Optional为空,表示操作失败        assertTrue(result.isEmpty(), "methodB should return empty Optional on failure");    }    // 如果有成功路径,可以这样测试    // @Test    // void testMethodB_successScenario() {    //     BRefactored b = new BRefactored();    //     Optional result = b.methodB();    //     assertTrue(result.isPresent(), "methodB should return non-empty Optional on success");    //     assertEquals("Operation successful", result.get());    // }}

方案二:重新抛出特定异常

如果内部异常代表了一个调用者应该知道并可能需要处理的错误情况,那么应该捕获原始异常并重新抛出封装后的、更具业务含义的异常。

Class B 重构示例 (重新抛出业务异常)

import org.slf4j.Logger;import org.slf4j.LoggerFactory;// 自定义业务异常class BusinessOperationException extends RuntimeException {    public BusinessOperationException(String message, Throwable cause) {        super(message, cause);    }}public class BRefactoredThrows {    private static Logger logger = LoggerFactory.getLogger("BRefactoredThrows");    public BRefactoredThrows() {        // 构造函数逻辑    }    /**     * 执行操作。如果内部发生错误,则抛出 BusinessOperationException。     * @throws BusinessOperationException 如果内部操作失败。     */    public void methodB() {        try {            // 模拟一个内部异常            throw new IllegalStateException("Critical internal state error");        } catch(Exception e) {            logger.error("Exception caught internally, rethrowing as BusinessOperationException: {}", e.getMessage());            // 捕获原始异常,封装并重新抛出            throw new BusinessOperationException("Failed to perform B operation", e);        }    }}

重构后 Class B 的测试示例现在可以使用 assertThrows 直接测试 methodB 抛出的业务异常:

import org.junit.jupiter.api.Test;import static org.junit.jupiter.api.Assertions.assertThrows;import static org.junit.jupiter.api.Assertions.assertTrue;public class BRefactoredThrowsTest {    @Test    void testMethodB_throwsBusinessOperationException() {        BRefactoredThrows b = new BRefactoredThrows();        // 断言 methodB 抛出 BusinessOperationException        BusinessOperationException thrown = assertThrows(            BusinessOperationException.class,            () -> b.methodB(),            "methodB should throw BusinessOperationException"        );        // 进一步验证异常信息或原因        assertTrue(thrown.getMessage().contains("Failed to perform B operation"));        assertTrue(thrown.getCause() instanceof IllegalStateException);    }}

替代方案:测试现有“坏设计”代码 (非理想但有时必要)

在某些情况下,我们可能无法立即重构现有代码,但仍需要为其编写测试。对于内部捕获并记录日志的异常,可以采用以下非理想但有时实用的测试策略。

方案一:验证日志输出

如果异常被捕获后会记录日志,那么可以通过验证日志系统是否接收到预期的错误消息来间接确认异常的发生。这通常需要一些额外的设置来捕获和检查日志。

概念性实现思路:

使用测试日志 Appender/Listener:许多日志框架(如 Logback, Log4j2)允许在测试环境中配置一个特殊的 Appender,它可以捕获日志事件并将其存储在内存中,供测试断言使用。模拟 Logger:使用 Mockito 等工具模拟 Logger 实例。如果 Logger 是通过依赖注入或可在测试中替换的方式获取的,可以模拟它来验证 error() 或 warn() 方法是否被调用,以及调用时传入的参数是否符合预期。

示例 (使用 Mockito 模拟 Logger)假设 Class B 可以通过构造函数注入 Logger:

import org.slf4j.Logger;import org.slf4j.LoggerFactory;public class BInjectableLogger {    private final Logger logger;    public BInjectableLogger(Logger logger) {        this.logger = logger;    }    public void methodB() {        try {            throw new Exception("NULL");        } catch(Exception e) {            logger.info("Exception thrown internally in BInjectableLogger", e);        }    }}

测试示例

import org.junit.jupiter.api.Test;import org.mockito.ArgumentCaptor;import org.slf4j.Logger;import static org.mockito.Mockito.*;import static org.junit.jupiter.api.Assertions.assertTrue;public class BInjectableLoggerTest {    @Test    void testMethodB_logsException() {        // 创建一个模拟的Logger        Logger mockLogger = mock(Logger.class);        BInjectableLogger b = new BInjectableLogger(mockLogger);        b.methodB();        // 验证logger.info方法是否被调用        // 捕获传递给info方法的参数        ArgumentCaptor messageCaptor = ArgumentCaptor.forClass(String.class);        ArgumentCaptor throwableCaptor = ArgumentCaptor.forClass(Throwable.class);        // 验证info方法至少被调用一次,并捕获参数        verify(mockLogger, times(1)).info(messageCaptor.capture(), throwableCaptor.capture());        // 断言日志消息和捕获的异常类型        assertTrue(messageCaptor.getValue().contains("Exception thrown internally"));        assertTrue(throwableCaptor.getValue() instanceof Exception);        assertTrue(throwableCaptor.getValue().getMessage().contains("NULL"));    }}

注意事项:如果 Logger 是通过 LoggerFactory.getLogger() 静态获取的,直接模拟会比较困难,可能需要 PowerMock 等工具,或者使用日志框架提供的测试工具。

方案二:使用 fail() 确保异常被捕获(基于原答案)

这种方法的目标是确保异常 确实被捕获没有逃逸 到调用的上层。如果 methodB 的 try-catch 块未能捕获到异常(例如,异常类型不匹配或 try 块逻辑改变导致异常在 catch 块外部抛出),那么测试将失败。

测试示例

import org.junit.jupiter.api.Test;import static org.junit.jupiter.api.Assertions.fail; // 导入 failpublic class ATestOriginalDesign {    @Test    void testMethodB_exceptionIsSwallowed() {        B b = new B(); // 使用原始的Class B        try {            b.methodB();            // 如果 methodB 内部的异常被成功捕获并处理,            // 那么代码会执行到这里,表示异常没有逃逸。            // 此时,我们不希望测试失败,所以这里不需要 fail()。            // 如果测试的目的是确保它 *不会* 抛出异常到外部,则此路径表示成功。            // 如果测试的目的是确保它 *内部* 抛出了异常,这种方法并不能直接验证。        } catch (Exception e) {            // 如果 methodB 内部的 catch 块没有捕获异常,            // 异常就会逃逸到这里,导致测试失败。            fail("Exception was not swallowed by B.methodB(): " + e.getMessage());        }    }    // 另一种更符合原答案意图的场景:    // 如果我们想测试一个方法,它 *应该* 捕获并处理某个异常,    // 但万一它没处理,我们就让测试失败。    @Test    void testMethodB_ensuresNoUnhandledExceptionEscapes() {        B b = new B(); // 原始的 Class B        try {            b.methodB();            // 如果代码执行到这里,说明 methodB 成功处理了内部异常,没有向外抛出。            // 对于一个“吞噬”异常的设计,这正是我们期望的行为,所以测试通过。        } catch (Exception e) {            // 如果 methodB 没有捕获异常,异常就会逃逸到这里。            // 此时,我们认为这是一个失败情况,因为 methodB 的设计目标是捕获它。            fail("Expected B.methodB() to swallow its internal exception, but it escaped: " + e.getMessage());        }    }}

此方法的局限性:这种方法实际上是在测试“异常没有逃逸”,而不是直接验证“内部抛出了异常”。对于 Class B 这种 总是 捕获异常的设计,testMethodB_ensuresNoUnhandledExceptionEscapes 这样的测试会 总是通过,因为它只是验证了 methodB 的 catch 块功能正常。它无法直接

以上就是如何测试内部捕获的异常的详细内容,更多请关注创想鸟其它相关文章!

版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。
如发现本站有涉嫌抄袭侵权/违法违规的内容, 请发送邮件至 chuangxiangniao@163.com 举报,一经查实,本站将立刻删除。
发布者:程序猿,转转请注明出处:https://www.chuangxiangniao.com/p/1073239.html

(0)
打赏 微信扫一扫 微信扫一扫 支付宝扫一扫 支付宝扫一扫
上一篇 2025年12月2日 06:56:06
下一篇 2025年12月2日 06:56:27

相关推荐

  • 如何使用Linux命令高效清理包含数十万张图片的目录?

    利用Linux命令行高效管理海量图片 本文介绍如何使用Linux命令行工具,快速清理包含数十万张图片的目录,并根据数据库记录筛选保留所需图片。 操作步骤: 提取有效图片URL: 从数据库中提取contents字段,将其转换为数组,并从中提取所有图片URL,保存至useful_urls.txt文件。 …

    2025年12月10日
    000
  • Go语言如何通过API重启Docker容器?

    Go语言与Docker API集成:实现代码变更后自动重启容器 在Swoole开发中,代码更新后快速重启Docker容器至关重要。本文介绍如何使用Go语言和Docker API实现此功能。 解决方案: Go语言的go-dockerclient库提供了与Docker API交互的便捷方式。 立即学习“…

    2025年12月10日
    000
  • ThinkPHP5 Windows服务器缓存写入失败怎么办?

    ThinkPHP5在Windows服务器缓存写入失败的解决方法 许多用户在Windows服务器环境下使用ThinkPHP5框架时,常常遇到缓存写入失败的问题,报错信息通常显示为file_put_contents(C:phpStudyPHPTutorialWWWappruntimecache4f819…

    2025年12月10日
    000
  • PDO插入后lastInsertId有值,但数据库却无数据,是什么原因?

    PDO::lastInsertId() 返回值不为零,但数据库中却没有插入数据? 使用PDO执行INSERT操作后,PDO::lastInsertId()方法返回一个非零值,却发现数据库中并没有对应的数据插入,这通常是由于以下几种原因导致的: 1. 事务回滚: 如果你的代码在执行INSERT语句后发…

    2025年12月10日
    000
  • PHP7版本更新对session处理有什么影响

    PHP7 对 Session 的优化带来了性能提升和安全性增强:性能优化:引擎优化减少了 Session 处理开销,提升了 Session 读写速度。安全性增强:对 Session ID 生成机制的调整增强了安全性,但仍需采取其他安全措施。潜在注意事项:迁移到 PHP7 可能存在兼容性问题,需要检查…

    2025年12月10日
    000
  • PHP 8如何进行安全部署

    如何在安全地部署 PHP 8 应用:代码层面:编写安全的代码,防止注入和跨站脚本攻击。服务器配置:禁用不必要的扩展,配置权限,使用 HTTPS,启用 HTTP 安全头。数据库安全:使用强密码,设置访问控制,进行备份,监控活动。定期安全审计:使用扫描工具,发现并修复漏洞。高级技巧:使用 OPcache…

    2025年12月10日
    000
  • PHP 8如何响应安全事件

    PHP 8 的安全事件响应需要遵循系统化的流程,包括:隔离受影响系统、收集证据、分析攻击、修复漏洞、恢复系统和持续监控。为了增强安全,应了解 PHP 安全函数、参数化查询、文件上传验证、会话管理,并考虑入侵检测系统。常见的错误包括依赖过时的库、忽略安全警告和不进行安全测试,应遵循最佳实践以实现安全和…

    2025年12月10日
    000
  • PHP 8如何建立安全意识

    PHP 8 的安全意识构建了一个多层次的防御体系,涵盖输入验证、数据过滤、输出编码、安全函数应用以及服务器配置,以应对安全风险,包括 SQL 注入、XSS、CSRF 和文件包含漏洞。通过采用严格的验证、过滤和编码措施,加上安全函数和适当的服务器配置,开发人员可以主动构建安全的应用程序,而非被动地修补…

    2025年12月10日
    000
  • PHP中的PSR-扩展编码样式指南

    PSR-12 扩展了 PSR-1 和已弃用的 PSR-2,为现代 PHP 提供了全面的编码规范。该标准通过定义特定的格式规则来确保 PHP 代码的一致性。 关键样式规则 常规代码布局:文件必须使用 Unix LF 行尾符。文件必须以单个空行结尾。必须省略 PHP 文件中的 <?php 标签(如…

    2025年12月10日
    000
  • 我在php中建造了`wc’

    最近,我尝试了John Crickett的编码挑战,并决定分享我的经验。第一个挑战是使用PHP重写经典的Unix工具wc(单词计数器)。虽然我自1997年以来就一直使用Linux,但wc并非我常用的工具,因此我决定深入研究一下。 我最初的想法是用文本编辑器直接编写代码,使用Vim在SSH连接下,平板…

    2025年12月10日
    000
  • 了解Laravel应用中的坚实原则

    Laravel应用中的SOLID原则:构建更健壮的应用 干净、易于维护的软件设计,其基石在于SOLID原则。这五个原则——单一职责原则(SRP)、开放封闭原则(OCP)、Liskov替换原则(LSP)、接口隔离原则(ISP)和依赖反转原则(DIP)——帮助开发者构建可扩展、可测试且易于维护的系统。本…

    2025年12月10日
    000
  • 在测试中使用Laravel中的PHP后备枚举

    PHP 枚举概述 PHP 8.1 引入了枚举,提供了一种定义命名值集合的结构化方式。Laravel 与枚举无缝集成,允许在模型、验证规则和查询条件中使用它们。然而,在测试中,一些细微之处可能会导致意外的失败。 示例:订单项目状态枚举 这是一个订单项目状态的后备枚举示例: namespace AppS…

    2025年12月10日
    000
  • 优化PHP应用程序:为什么单独阅读和写入模型很重要

    模型是与数据存储交互的理想工具。它们定义数据的结构,确保与数据存储(通常是数据库)兼容。模型不仅验证输入数据,辅助数据写入,还能用于数据检索。然而,除了简单的CRUD应用之外,将同一个模型用于读写通常并非最佳实践。让我们深入探讨原因。 创建模型 让我们以一个简单的用户模型和存储库接口为例,这里无需详…

    2025年12月10日
    000
  • PHP本地开发工具5

    > phpstudy Web:Web开发的综合工具 PhPstudy Web是一种非常流行且用户友好的软件,旨在帮助开发人员有效地设置和管理Web服务器和PHP环境。 PhpStudy Web以其简单性和多功能性而闻名,在网络开发社区中广泛使用,尤其是用于本地开发和测试。 什么是phpstud…

    2025年12月10日
    000
  • 为什么我讨厌WordPress,但是为什么它仍然很棒和必要

    WordPress占据着超过40%的网站市场份额,是目前最流行的内容管理系统(CMS)。然而,在软件工程师群体中,特别是后端开发和可扩展Web应用领域的工程师,WordPress因其低效、臃肿和令人沮丧的特性而臭名昭著。 我个人非常不喜欢WordPress。我花费了大量时间处理其混乱的代码库、解决插…

    2025年12月10日
    000
  • PHP 8如何进行数据库连接安全

    要安全地连接 PHP 8 数据库,需要保护凭据并防止 SQL 注入:使用预处理语句分离 SQL 查询和数据,以避免 SQL 注入。使用密码哈希存储密码,防止泄露。遵循最小权限原则,限制用户的数据库访问权限。使用 HTTPS 加密数据传输。验证用户输入,防止恶意数据进入。 PHP 8 数据库连接安全:…

    2025年12月10日
    000
  • 设计一个数字容器系统

    设计一个高效的数字容器系统,支持以下操作: 插入/替换: 将指定索引处的值替换为新值。如果索引不存在,则插入新值。查找最小索引: 返回给定数字在容器中出现的最小索引。如果数字不存在,则返回 -1。 挑战难度: 中等 相关主题: 哈希表,设计模式,最小堆(优先队列) 示例: [“NumberConta…

    2025年12月10日
    000
  • 与作曲家制作和共享PHP库

    Composer已成为PHP项目依赖管理和代码复用的核心工具。无论您是贡献开源项目还是提升个人开发效率,学习创建Composer包都是一项非常有价值的技能。本文将引导您完成构建和共享个人PHP库的完整流程。 准备工作 在开始之前,请确保您已具备以下条件: 扎实的PHP和Composer基础知识。已在…

    2025年12月10日
    000
  • 通过Laravel和Livewire邀请开发ERP

    大家好, 我最近完成了一个基于Web的计费系统项目,使用Laravel和Livewire框架构建。最初,这个项目只是为了满足朋友的需求,帮他创建一个简单的客户交易记录系统。 我通过在数据库中存储产品信息,然后将这些产品添加到发票中来实现发票/账单的创建功能。 随着项目的进展,我逐步添加了更多功能,例…

    2025年12月10日
    000
  • Laravel注入命令:如何检测和防止它

    Laravel 命令注入漏洞:检测与防御 命令注入是严重的服务器端安全漏洞,允许攻击者执行任意系统命令。如果 laravel 应用在处理系统命令时未妥善处理用户输入,则极易受到此类攻击。本文将深入探讨命令注入,提供代码示例,并讲解如何保护您的 laravel 应用免受此类威胁。 我们还将介绍一款免费…

    2025年12月10日
    000

发表回复

登录后才能评论
关注微信