【JUnit实战3_16】第九章:容器内测试(上)

《JUnit in Action》全新第3版封面截图
写在前面
这一章给我的感觉是对前面两种模拟技术的延伸,并且更贴近真实的工作场景,毕竟企业级 Java 应用几乎天天都在和各种形式的容器打交道。做惯简单的play-test编程人员,一开始见识这种正规军的打法难免会有点不适应,甚至总有点吐槽的冲动——搞那么复杂,还不如手动测试来得过瘾……吐槽归吐槽,该纠偏还得纠偏,不然学到这里打退堂鼓就真的亏大了。
文章目录
- 第九章 容器内测试(上)
- 9.1 容器内测试的应用场景
- 9.2 容器内测试的基本流程
- 9.3 Stub 模拟、mock 对象模拟、容器内测试横向对比
- 9.3.1 Stub 桩代码模拟的优缺点
- 9.3.2 mock 对象模拟的优缺点
- 9.3.3 容器内测试的优缺点
第九章 容器内测试(上)
本章概要
mock对象模拟的局限性分析- 容器内测试的用法
Stub模拟、mock对象模拟及容器内测试的横向评估对比Arquillian框架用法简介
The secret of success is sincerity. Once you can fake that you’ve got it made.
成功的秘诀在于真诚。一旦能伪装出真诚,离成功也就不远了。—— Jean Giraudoux
本章探讨了一种在应用容器内对组件进行单元测试的方法:容器内单元测试或集成测试。这里的容器不是像 Docker 那样的容器,而是像 Jetty 或 Tomcat 这样的 servlet 容器,或者像 JBoss(已更名为 WildFly)那样的企业级 Java Bean(即 EJB)容器。本章还会对比容器内测试和 Stub 桩模拟、mock 对象模拟的优缺点,并介绍 Arquillian 框架 —— 一种与具体容器无关的、专门用于集成测试的 JavaEE 框架的用法。
9.1 容器内测试的应用场景
对容器内测试的迫切需求,源于常规单元测试的局限性。例如,在 servlet 语境下,如果一个类继承了抽象类 javax.servlet.http.HttpServlet,并重写了 isAuthenticated(HttpServletRequest request) 方法:
public class SampleServlet extends HttpServlet {private static final long serialVersionUID = 1L;public boolean isAuthenticated(HttpServletRequest request) {HttpSession session = request.getSession(false);if (session == null) {return false;}String authenticationAttribute = String.valueOf(session.getAttribute("authenticated"));return Boolean.parseBoolean(authenticationAttribute);}
}
按照此前 mock 对象模拟的思路,借助 Mockito 框架写出的测试用例大致如下:
@ExtendWith(MockitoExtension.class)
public class TestSampleServletWithMockito {@Mockprivate HttpServletRequest request;@Mockprivate HttpSession session;private SampleServlet servlet;@BeforeEachpublic void setUp() {servlet = new SampleServlet();}@Testpublic void testIsAuthenticatedAuthenticated() {when(request.getSession(false)).thenReturn(session);when(session.getAttribute("authenticated")).thenReturn("true");assertTrue(servlet.isAuthenticated(request));}@Testpublic void testIsAuthenticatedNotAuthenticated() {when(request.getSession(false)).thenReturn(session);when(session.getAttribute("authenticated")).thenReturn("false");assertFalse(servlet.isAuthenticated(request));}@Testpublic void testIsAuthenticatedNoSession() {when(request.getSession(false)).thenReturn(null);assertFalse(servlet.isAuthenticated(request));}
}
上述代码通过在每个测试用例中分别初始化 mock 对象、设置期望值、最后执行断言,isAuthenticated() 方法的核心逻辑全部得以验证通过,看起来没什么问题。
但是问题就出在对这些对象的模拟上:因为被测系统是在 servlet 语境下运行的,要测试 isAuthenticated() 需要一个有效的 HttpServletRequest 对象及 HttpSession 对象;而 HttpServletRequest 只是一个接口,其生命周期和具体实现是由所在的 servlet 容器提供的。只要这些对象是在容器运行时创建并管理的,测试人员就应该模拟出一个与真实场景差不多的容器环境,最好还能追踪这些对象的状态,让测试用例在一个相对真实的环境下运行,而不是脱离容器、仅凭常规的 JUnit 技术(即 JUnit 5 的功能特性、Stub 桩和 mock 对象模拟等)来测试它们。
上述 mock 对象的测试方法虽然也没问题,但遇到真实的复杂业务逻辑,运行测试前很可能需要设置大量的期望值来还原当时的容器环境,导致编写大量的辅助代码,淹没真正的测试逻辑。虽然人们总希望对容器行为的模拟尽可能精简,但往往事与愿违。一旦 servlet 环境出现变动,这些预设的期望值也必须同步更新,无形中加大了测试编码的编写难度和后期的运维成本。
总之,当代码与容器存在交互,且测试无法创建有效的容器对象(如 HttpServletRequest)时,应该首选 容器内测试(in-container testing)。
9.2 容器内测试的基本流程
测试 SampleServlet 最理想的方案是在一个 servlet 容器中运行测试用例,此时无需模拟 HttpServletRequest 和 HttpSession,而是可以直接在真实容器中访问所需的对象和方法。这就要寻求某种机制,让测试用例可以部署到容器中执行。
容器内测试大致有两种架构驱动模式:
- 服务端驱动
- 客户端驱动
以下为客户端驱动的示意图:

基于客户端驱动的容器内测试的生命周期(基本流程)大致如下:
- 执行客户端测试类;
- 调用服务器端相同的测试用例(通过
HTTP(S)等协议); - 测试领域对象;
- 将结果响应给客户端。
补充:服务端驱动的基本流程
根据
DeepSeek提供的思路,服务端驱动的大致流程如下(待验证):
其基本流程概括如下:
- 打包并部署:将包含测试代码的应用部署到服务器。
- 从外部触发:通过测试客户端(通常为一个脚本或一个简单的
Java程序)触发容器内的测试协调器。- 在容器内部:由协调器利用
JUnit执行测试类,并通过测试类调用真实的业务组件实施测试。- 收集结果:最终生成测试报告,供外部客户端消费。
9.3 Stub 模拟、mock 对象模拟、容器内测试横向对比
9.3.1 Stub 桩代码模拟的优缺点
| 优点 | 缺点 |
|---|---|
| 快速且轻量 | 需要专门的方法来验证状态 |
| 易于编写和理解 | 无法测试模拟对象(faked objects)的行为 |
| 功能强大 | 在复杂交互场景下耗时较长 |
| 适合更粗粒度的测试 | 代码变更时需要更多维护 |
9.3.2 mock 对象模拟的优缺点
| 优点 | 缺点 |
|---|---|
| 测试执行无需依赖容器的运行 | 无法测试与容器的交互,以及组件间的交互 |
| 配置和运行速度快 | 不测试组件的部署 |
| 支持更细粒度的单元测试 | 需充分了解待模拟的 API(尤其对外部库而言可能很困难) |
| 无法保证代码能在目标容器中运行 | |
| 更细粒度的测试也意味着测试代码很可能被接口淹没 | |
代码变更时需要投入更多维护(与 Stub 模拟类似) |
9.3.3 容器内测试的优缺点
主要优势:提供容器环境方便测试。
劣势:
- 需要特定的工具支持:尽管概念本身具备通用性,但具体的实现工具则因容器
API而不同:基于servlet容器需用Jetty、Tomcat;基于EJB容器需用WildFly等等; - 与
IDE的集成欠佳:常用Maven/Gradle在嵌入式容器中执行测试,或在持续集成服务器中运行build构建流程。业内普遍缺乏良好的IDE支持; - 执行时间较长:由于测试在容器中运行,需要启动和管理容器,这可能会很耗时;
- 配置复杂:这也是容器内测试的最大缺点。为了让应用及其测试在容器中运行,项目必须打包(如
war或ear文件)并部署到容器中;然后必须启动容器、运行测试。最佳实践是将该测试流程自动化并纳入整个构建体系。
(未完待续,下篇重点梳理 Arquillian 框架的基本用法,敬请关注)

