详细介绍:【JUnit实战3_27】第十六章:用 JUnit 测试 Spring 应用:通过实战案例深入理解 IoC 原理

news/2025/12/1 12:07:38/文章来源:https://www.cnblogs.com/yangykaifa/p/19292296

JUnit in Action, Third Edition

《JUnit in Action》全新第3版封面截图

写在前面
本书前 15 章内容都可以作为铺垫,对于 Java 开发者而言,真正的重点从这一章才算开始。作者出于知识点全覆盖的考虑,从 Spring 框架最原始的 XML 配置开始,聚焦 Spring 和 JUnit 单元测试最关心的控制反转(IoC,即依赖注入)机制,结合两个典型案例进行了深入全面的介绍,非常具有参考价值。

第十六章:测试 Spring 应用

本章概要

  • 深入理解依赖注入原理;
  • Spring 应用的构建与测试方法;
  • 通过 SpringExtension 启用 JUnit JUpiter 的方法;
  • 使用 JUnit 5 相关特性测试 Spring 应用。

“Dependency Injection” is a 25-dollar term for a 5-cent concept.
“依赖注入”是用二十五美元的术语描述的一个五美分的概念。

—— James Shore1

本章对 Spring 框架中最核心的依赖注入设计进行了详细介绍,并结合两个典型案例加深理解。

16.1 Spring 框架简介

Rod Johnson 于 2003 年在其著作《Expert One-on-One J2EE Design and Development》中首次提出了 Spring 框架,其设计理念在于简化传统企业级应用开发。

业务代码(即开发者写的代码)、库函数与框架间的调用关系示意图如下:

Fig16.1

框架的作用在于提供某种开发范式,助力开发者更专注于业务本身的开发,而不过分关注架构设计方面的问题。

16.2 关于依赖注入

Java 应用的良好运转离不开对象间的相互协作,可惜 Java 语言本身无法自行组织一款应用程序的基本要素,Spring 出现前这部分工作是由开发者或架构师亲自负责实现的,涉及各种必要的设计模式和架构考虑等,心智负担较重。

Spring 框架实现了多种设计模式,尤其是 依赖注入(dependency injection 模式(又称 控制反转(IoC、Inversion of Control)让 Spring 框架完成这类枯燥繁琐的组织工作成为了可能,真正解放了开发者的生产力。

16.3 依赖注入的简单案例

考察如下乘客管理示例应用中的乘客实体类 Passenger 及国家实体类 Country

public class Passenger {
private String name;
private Country country;
public Passenger(String name) {
this.name = name;
this.country = new Country("USA", "US");
}
public String getName() {
return name;
}
public Country getCountry() {
return country;
}
}
public class Country {
private String name;
private String codeName;
public Country(String name, String codeName) {
this.name = name;
this.codeName = codeName;
}
public String getName() {
return name;
}
public String getCodeName() {
return codeName;
}
}

写成 L7 这样后的主要问题:

  • Passenger 直接依赖 Country
  • 测试时 Passenger 无法与 Country 相隔离;
  • Country 实例的生命周期严重依赖于 Passenger
  • 测试时无法将 Country 替换为其他对象;

正确的写法应该是:

public class Passenger {
private String name;
private Country country;
public Passenger(String name) {
this.name = name;
}
public String getName() {
return name;
}
public Country getCountry() {
return country;
}
public void setCountry(Country country) {
this.country = country;
}
}

这样 country 的取值完全由 setter 方法控制,Passenger 不再直接依赖于 Country

16.4 IoC 实战一:只用 Spring 中的类

PassengerCountry 解耦后,再通过一个对比案例,看看遵循 IoC 原则去实例化 Passenger,和传统方式实例化究竟有没有区别。

为了顺便演示两个框架的演变过程,这里先用旧版 Spring4 + JUnit 4 搭建测试环境。

配置旧版环境 Maven 依赖:

<!-- Spring framework 4 --><dependency><groupId>org.springframework</groupId><artifactId>spring-context</artifactId><!-- 提供 ClassPathXmlApplicationContext 类 --><version>4.2.5.RELEASE</version></dependency><!-- JUnit 4 --><dependency><groupId>junit</groupId><artifactId>junit</artifactId><!-- 提供 @Before、@Test --><version>4.12</version></dependency>

传统 IoC 实例化方式如下:

public class PassengerUtil {
public static Passenger getExpectedPassenger() {
Passenger passenger = new Passenger("John Smith");
Country country = new Country("USA", "US");
passenger.setCountry(country);
return passenger;
}
}

然后再用 Spring 来实现一个等效对象(从最原始的 XML 配置文件开始)——

先在 CLASSPATH 下创建 XML 格式的配置文件 src/test/resources/application-context.xml

<?xml version="1.0" encoding="UTF-8"?><beans xmlns="http://www.springframework.org/schema/beans"xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd"><bean id="passenger" class="com.manning.junitbook.spring.Passenger"><constructor-arg name="name" value="John Smith"/><property name="country" ref="country"/></bean><bean id="country" class="com.manning.junitbook.spring.Country"><constructor-arg name="name" value="USA"/><constructor-arg name="codeName" value="US"/></bean>
</beans>

然后读取该 XML 配置,并通过应用上下文 context 来实例化 Passenger(即依赖注入):

public class PassengerUtil {
public static Passenger getActualPassenger() {
ClassPathXmlApplicationContext context = new ClassPathXmlApplicationContext(
"classpath:application-context.xml");
return context.getBean("passenger", Passenger.class);
}
}

完整的测试逻辑如下:

public class SimpleAppTest {
private static final String APPLICATION_CONTEXT_XML_FILE_NAME = "classpath:application-context.xml";
private ClassPathXmlApplicationContext context;
private Passenger expectedPassenger;
@Before
public void setUp() {
context = new ClassPathXmlApplicationContext(APPLICATION_CONTEXT_XML_FILE_NAME);
expectedPassenger = getExpectedPassenger();
}
@Test
public void testInitPassenger() {
Passenger passenger = context.getBean("passenger", Passenger.class);
assertEquals(expectedPassenger, passenger);
System.out.println(passenger);
}
}

实测结果:

Fig16.2

注意:这里的 assertEquals() 断言之所以能通过,是因为 PassengerCountry 实体重写了 equals() 方法和 hashCode() 方法,只比较属性值,不涉及对象的引用。

16.5 IoC 实战二:引入 Spring 4 注解

Spring TestContext 框架是 Spring 框架对单元测试和集成测试做的集成,支持多种测试框架(JUnit 3.xJUnit 4.xTestNG 等)。为此需要调整以下 Maven 依赖:

<!-- Spring framework 4 --><dependency><groupId>org.springframework</groupId><artifactId>spring-context</artifactId><!-- 提供 @Autowired --><version>4.2.5.RELEASE</version></dependency><dependency><groupId>org.springframework</groupId><artifactId>spring-test</artifactId><!-- 提供 SpringJUnit4ClassRunner、@ContextConfiguration --><version>4.2.5.RELEASE</version></dependency><!-- JUnit 4 --><dependency><groupId>junit</groupId><artifactId>junit</artifactId><!-- 提供 @RunWith、@Before、@Test --><version>4.12</version></dependency>

上述案例套用 Spring 注解的等效版本为:

@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration("classpath:application-context.xml")
public class SpringAppTest {
@Autowired
private Passenger passenger;
private Passenger expectedPassenger;
@Before
public void setUp() {
expectedPassenger = getExpectedPassenger();
}
@Test
public void testInitPassenger() {
assertEquals(expectedPassenger, passenger);
System.out.println(passenger);
}
}

其中——

  • @ContextConfiguration 注解来自 spring-test 依赖;
  • @Autowired 注解来自 spring-context 中的 spring-bean 依赖;
  • @RunWith 来自 JUnit 4.x

还可以从 IDEA 的依赖分析图中更直观地查看 Spring 注解所属的 Maven 依赖:

Fig16.3

可以看到,传统方式如果要修改 expectedPassenger 实例,必须修改源代码并重新编译项目才能生效;而引入 Spring 容器后,注入新的 passenger 实例无需改动源代码更无需重新编译,只需修改 XML 配置文件即可。

同时,测试类通过 @AutoWired 注入 passenger 依赖后,测试方法就不再以代码的方式干预 passenger 的实现细节了(由 Spring 注入),更不用关心 passenger 和它的 country 属性是怎么组合的(XML 配置);只需要和 expectedPassenger 直接比较就行了(核心逻辑)。

两种实例化方式的具体对比如下:

传统方式Spring DI 方式
创建细节Passenger 知道如何创建 Country两者都不知道对方如何创建
依赖关系Passenger 硬编码依赖 Country依赖关系由外部配置决定
具体实现Passenger 依赖具体的 Country自动装配还可以依赖接口,实现是可替换的
测试难度难以单独测试 Passenger可以轻松模拟 Country 进行测试

16.6 IoC 实战三:升级到 Spring 5 + JUnit 5

Maven 依赖升级到 Spring 5JUnit 5

<!-- Spring 5 --><dependency><groupId>org.springframework</groupId><artifactId>spring-context</artifactId><version>5.2.0.RELEASE</version></dependency><dependency><groupId>org.springframework</groupId><artifactId>spring-test</artifactId><version>5.2.0.RELEASE</version></dependency><!-- JUnit 5 --><dependency><groupId>org.junit.jupiter</groupId><artifactId>junit-jupiter-api</artifactId><version>5.6.0</version><scope>test</scope></dependency><dependency><groupId>org.junit.jupiter</groupId><artifactId>junit-jupiter-engine</artifactId><version>5.6.0</version><scope>test</scope></dependency>

等效测试类如下:

@ExtendWith(SpringExtension.class)
@ContextConfiguration("classpath:application-context.xml")
public class SpringAppTest {
@Autowired
private Passenger passenger;
private Passenger expectedPassenger;
@BeforeEach
public void setUp() {
expectedPassenger = getExpectedPassenger();
}
@Test
public void testInitPassenger() {
assertEquals(expectedPassenger, passenger);
System.out.println(passenger);
}
}

16.5 小节的主要区别:

  • @ContextConfiguration@Autowired 均为 Spring 5.x 框架下的注解;
  • 测试用例使用 JUnit 5Extension API@ExtendWith(SpringExtension.class)
  • @BeforeEach@Test 均为 JUnit 5 版本。

实测结果:

Fig16.4

16.7 Spring 5 + JUnit 5 实战:实现观察者模式

需求描述:示例项目成功注册一名乘客后(简化为乘客类的实例化),需要通过经办人(registerManager)回应一则确认消息给乘客。

该需求可以通过 观察者模式(Observer pattern 来实现:Passenger 类实例化成功后,由 registerManager 推送一个反馈事件;关注该事件的观察者(未必是乘客本人)从事件中变更乘客状态为 已确认,然后以某种方式回应该事件(如控制台打印一则消息)。

观察者模式

在该模式下,被观察主体(subject)会主动维护一组依赖(dependents,即观察者或监听器)。当主体方触发一个依赖方关注的事件时,就会通知这些观察者;观察者通过各自的 Listener 方法响应收到的事件,实现各自的响应逻辑。

被观察者和观察者的相互作用如下图所示:

Fig16.6

具体到本示例中就是:

Fig16.7

Spring 框架下,registerManager 可以通过 ApplicationContext 接口的 pushEvent(event) 方法推送某个事件;事件接收方通过添加 @EventListener 注解关联到具体的事件响应逻辑(变更乘客状态,并打印一则确认信息)。

因此,该需求可拆解为以下几个子任务:

  • 主客体识别:registerManager 为主体(被观察者),PassengerRegistrationListener 为客体(观察者);
  • registerManager 推送一个 注册事件,客体接收该事件并实现反馈逻辑:
    • 变更 Passenger 的注册状态(需新增一个 isRegistered 字段);
    • 控制台打印一则消息,表示已经注册成功。
  • 新增注册事件实体,要求能从该事件获取到带确认的 passenger 对象;
  • 修改 XML 配置完成指定包路的 Bean 扫描;
  • 编写测试用例,通过依赖注入给 passengerregisterManager 赋值,并推送一则注册事件。

具体实现如下:

首先改造 Passenger 实体类,新增一个标记属性 isRegistered,并补全 gettersetter 等方法:

public class Passenger {
private String name;
private Country country;
private boolean isRegistered;
// -- snip --
public boolean isRegistered() {
return isRegistered;
}
public void setIsRegistered(boolean isRegistered) {
this.isRegistered = isRegistered;
}
@Override
public String toString() {
return "Passenger{name='" + name + "'\', country=" + country + ", registered=" + isRegistered + "}";
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
Passenger passenger = (Passenger) o;
return isRegistered == passenger.isRegistered &&
Objects.equals(name, passenger.name) &&
Objects.equals(country, passenger.country);
}
@Override
public int hashCode() {
return Objects.hash(name, country, isRegistered);
}
}

然后新增 RegistrationManager 类,并关联一个 applicationContext 属性:

@Service
public class RegistrationManager implements ApplicationContextAware {
private ApplicationContext applicationContext;
public ApplicationContext getApplicationContext() {
return applicationContext;
}
@Override
public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
this.applicationContext = applicationContext;
}
}

再新增一个乘客注册事件类 PassengerRegistrationEvent,并关联一个 passenger 属性并作为事件源(source):

public class PassengerRegistrationEvent extends ApplicationEvent {
private Passenger passenger;
public PassengerRegistrationEvent(Passenger passenger) {
super(passenger);
this.passenger = passenger;
}
public Passenger getPassenger() {
return passenger;
}
public void setPassenger(Passenger passenger) {
this.passenger = passenger;
}
}

接着创建一个观察者类 PassengerRegistrationListener,在响应方法中接收该事件,并实现要求的事件响应逻辑:

@Service
public class PassengerRegistrationListener {
@EventListener
public void confirmRegistration(PassengerRegistrationEvent passengerRegistrationEvent) {
passengerRegistrationEvent.getPassenger().setIsRegistered(true);
System.out.println("Confirming the registration for the passenger: "
+ passengerRegistrationEvent.getPassenger());
}
}

再修改 application-context.xml 配置文件,实现指定包路的自动扫描:

<?xml version="1.0" encoding="UTF-8"?><beans xmlns="http://www.springframework.org/schema/beans"xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:context="http://www.springframework.org/schema/context"xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsdhttp://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context.xsd"><bean id="passenger" class="com.manning.junitbook.spring.Passenger"><constructor-arg name="name" value="John Smith"/><property name="country" ref="country"/><property name="isRegistered" value="false"/></bean><bean id="country" class="com.manning.junitbook.spring.Country"><constructor-arg name="name" value="USA"/><constructor-arg name="codeName" value="US"/></bean><!-- 自动扫描 com.manning.junitbook.spring 包路下的 Bean 定义 --><context:component-scan base-package="com.manning.junitbook.spring" /></beans>

最后编写测试用例,在核心逻辑中发起一次事件推送:

@ExtendWith(SpringExtension.class)
@ContextConfiguration("classpath:application-context.xml")
public class RegistrationTest {
@Autowired
private Passenger passenger;
@Autowired
private RegistrationManager registrationManager;
@Test
public void testPersonRegistration() {
System.out.println("Before registering: " + passenger);
registrationManager.getApplicationContext().publishEvent(new PassengerRegistrationEvent(passenger));
System.out.println("After registering:");
System.out.println(passenger);
assertTrue(passenger.isRegistered());
}
}

注意事项:

  • passenger 的实例化依旧通过之前的 XML 文件来完成;
  • 最终的事件响应逻辑由标注了 @EventListenerconfirmRegistration() 方法具体实现;
  • 被观察主体 registrationManager 和观察者客体 PassengerRegistrationListener 的实例化均由 @Service 注解完成;为此,需要在 application-context.xml 中添加一个自动扫描 Bean 定义的标签 <context:component-scan />
  • registrationManagerPassengerRegistrationListener 的关联,并没有显式调用示意图中的 attach(listener) 方法,而是通过 @EventListener 注解Spring 的应用上下文 自动建立的。该过程是在 Spring 扫描到 @EventListener 注解后,由 Spring 框架自动关联的。

实测效果:

Fig16.5


  1. James Shore 是一位在软件开发和敏捷方法领域备受尊敬的顾问、演讲者和作者,著有《The Art of Agile Development》(中译本《敏捷开发的艺术》)。因其在敏捷软件开发、特别是测试驱动开发和持续集成方面的深刻见解和实践经验而闻名。 ↩︎

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/982811.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

2025年哈尔滨全屋定制品牌十大排行榜,久木定制测评推荐

为帮黑龙江家庭高效锁定适配自身需求的全屋定制合作伙伴,避免装修走弯路,我们从板材品质(如环保等级、品牌授权)、空间设计能力(含户型适配、风格一体化)、价格透明度(增项标注、报价逻辑)及真实客户口碑(侧重…

中电金信:这个AI“专家系统”,让智能体真正懂金融、落地可控

目前,AI正以指数级速度进化,从模型走向智能体时代。智能体如雨后春笋般涌现,上千款应用正在重塑各行各业,然而,在规则严谨、流程复杂的金融领域,AI想真正落地,并不只是“部署一个模型”那么简单。一家金融机构的…

K-D Tree 相关

讲解 K-D Tree 相关。部分发表于洛谷。 简介: K-D Tree 是一种适用于 \(k\) 维空间信息处理的数据结构,一般是维护 \(n\) 个点的信息,建出平衡二叉树;在 \(k\) 比较小的 建树: 一般使用交替建树,递归的分为以下三…

TopDiag P181 Wire Finder: Effortlessly Locate Automotive Wire Breakpoints Short Circuits

Troubleshoot Electrical Woes Faster: Introducing the TopDiag P181 Wire Finder The Hidden Cost of Electrical Breakdowns For automotive mechanics and car owners, tracking down a short circuit or a hidden…

2025年最新工业冷风机性能排行榜TOP10,生产车间厂房降温/橡胶车间通风降温/车间厂房工厂通风降温工业冷风机厂商推荐榜单

行业洞察 随着工业4.0时代的深入发展,工业冷风机作为厂房车间通风降温的核心设备,其性能表现直接关系到生产环境优化与能源消耗控制。基于2024-2025年市场公开数据与产品实测表现,本文从技术参数、能效比、适用场景…

麒麟V10服务器配置网络 - 华

银河麒麟V10服务器系统默认集成network和Network Manager两种网络管理工具。network基于Shell脚本 , 通过修改/etc/sysconfig/network-scripts 目录下的配置文件来管理网络连接;Network Manager是一种较新的网络连接管…

在AI技术唾手可得的时代,挖掘安全测试新需求成为关键——某知名Web安全训练平台需求探索

本文分析了一个广泛使用的Web安全训练平台的核心功能和用户反馈,揭示了在AI技术快速发展的背景下,用户对多语言支持、容器化部署、新漏洞类型和现代化界面等方面的潜在需求,这些需求反映了安全测试工具在新时代的发…

临时文件发送工具(wormhole.app)24小时有效

临时文件发送工具(wormhole.app)24小时有效https://wormhole.app/

7.QML组件Slider

7.QML组件SliderWindow {visible: truewidth: 640height: 480title: qsTr("QML demo")Slider {id: controlvalue: 0.5background: Rectangle {x: control.leftPaddingy: control.topPadding + control.avail…

基于Matlab的压缩感知信道估计算法实现

一、压缩感知信道估计原理概述 在无线通信系统中,信道估计是接收端补偿信道衰落、恢复发送信号的关键步骤。传统方法(如最小二乘LS、最小均方误差MMSE)需密集导频,开销大。压缩感知(Compressed Sensing, CS)利用…

2025黑龙江洁净工程公司TOP5权威推荐:专业测评净朗净化

洁净工程是医疗与工业领域的隐形基石,其质量直接关乎医疗安全、产品精度与作业效率。2024年数据显示,黑龙江医疗洁净工程市场需求年增速达32%,工业洁净场景占比提升至45%,但行业投诉中60%集中在净化效率不达标、后…

2025年广州优质精装现楼厂房租赁排行榜,资质齐全/售后完善

为帮助企业快速找到适配自身发展需求的产业载体,避免因厂房选型不当导致的生产延误、运营成本超支等问题,我们从资质合规性(如产权明晰度、消防验收标准)、售后保障能力(含维修响应时效、配套服务持续性)、客户口…

ASCIIMoon

ASCIIMoon是一个展示月亮每日相位变化的网站,其最大特色是使用ASCII字符艺术来呈现月相(新月、弯月、满月)。这个网站将天文学与字符艺术巧妙结合,为用户提供了一种复古而有趣的观察月相的方式。网址:https://asc…

2025年RFID衣物洗涤标签供货厂家权威推荐榜单:酒店洗涤RFID标签‌/RFID洗涤耐高压标签‌/RFID布草智能管理‌源头厂家精选

在布草智能管理需求日益增长的2025年,RFID衣物洗涤标签已成为酒店、医院和洗涤工厂实现精准化管理的核心工具。据行业报告显示,应用RFID技术后,布草库存盘点效率可提升80%以上,人工成本显著降低。 在酒店布草、医疗…

UML进阶:深入理解类图和序列图

作为软件工程专业的学生,我们通常都会接触到统一建模语言(UML)。UML是一种标准的建模语言,用于软件工程领域中的视觉建模。在这篇文章中,我将分享一些UML的进阶知识,特别是关于类图和序列图的理解。 什么是UML?…

进口还是国产?阿迩法与爱柯尼OK镜深度解析

角膜塑形镜是目前青少年近视控制的常用工具之一,不同品牌的产品在技术、适配性等方面各有特点。以下是两款热门角膜塑形镜的详细介绍: 一、爱柯尼新一代角膜塑形镜(中国品牌)作为国产角膜塑形镜产品,爱柯尼主打“…

TOPDIAG P200 Pro: New Generation Intelligent Circuit Detector for All 9V-48V Cars, Trucks Boats

Problem: Diagnosing Modern Vehicle Circuits—A Complex Challenge In today’s automotive landscape, electrical systems power everything from compact cars to heavy trucks, boats, and construction machine…

2025年平板运输车制造企业权威推荐榜单:遥控平板车‌/顶升电动平车‌/升降电动平车‌源头厂家精选

在智能制造与绿色物流的双重驱动下,2024年中国电动平板车市场规模已突破200亿元,预计2025年将保持15%以上的年增长率,遥控平板车、顶升电动平车与升降电动平车正成为工业搬运领域的核心装备。 在工业4.0和物流智能化…

代码传递

from jqdata import * import matplotlib.pyplot as plt import seaborn as sns import pandas as pd import datetime import warnings import numpy as np from tqdm import tqdm import gc plt.rcParams[font.sans-…

深圳“无人机装调检修工”报考入户全指南:证书含金量超高!

“无人机装调检修工”职业技能等级证书已成为落户深圳的“黄金敲门砖”!全市考点现正开放报考,持证不仅可申领最高数千元的政府补贴,符合条件者更能直接申请入户深圳,享受一线城市全方位福利。 一、 谁适合报考?瞄…