测试Spring应用程序
JUnit 5
使用JUnit 5编写测试
在实验中均使用JUnit 5进行测试
- 支持Junit 5 是Spring 5的一个主要特征
- JUnit 5是Spring Boot 2.2版本开始的默认JUnit版本
- 需要Java 8 + 运行环境
- 利用Lambda
组件
JUnit 平台 在JVM上启动测试框架的基础
JUnit Jupiter 在JUnit 5中编写测试和扩展模型
JUnit Vintage 用于在平台上运行JUnit 3和4测试的TestEngine
JUnit 5:新编程模型(JUnit 5忽略了所有JUnit 4注解)
替换JUnit 4注解
介绍新注解
单元测试(没有Spring的单元测试)
- 测试功能的一个单元
- 保持最少的依赖项
- 与环境(包括Spring)隔离
- 使用简化的依赖项替代方案 Stubs 与/或 Mocks
集成测试(使用Spring的集成测试)
集成测试
测试多个单元共同工作的互动 首先所有单元都能单独运行(单元测试显示了这一点)
在其周围的基础架构的上下文中测试应用程序类
- 容器外的测试,不需要运行整个应用程序服务器
- 基础架构可能“缩小” 使用ActiveMQ代替商业消息服务器
- 测试容器 使用轻量级的,可抛弃的普通数据库实例,或其它任何可以在Docker容器中运行的东西
集成测试示例
Spring对测试的支持
Spring有丰富的测试支持
- 基于TestContext框架 为你的测试定义一个ApplicationContext
- @ContextConfiguration 定义需要使用的Spring配置
作为一个单独的模块打包
spring-test.jar
JUnit 5中的@ExtendWith
JUnit 5有通过@ExtendWith的可扩展框架
- 取代JUnit 4的@RunWith
- JUnit 5支持多种扩展-因此名称进行升级
Spring的扩展点是SpringExtension类 一个感知Spring的测试运行者
@SpringJUnitConfig
@SpringJUnitConfig是一个“合成”注解,他整合了
- 来自JUnit 5的@ExtendWith(SpringExtension.class)
- 来自Spring的@ContextConfiguration
在测试中自动布线的替代方案
将配置作为一个内部类
多个测试方法
脏上下文
在测试方法结束时强制关闭上下文 允许测试@PreDestroy行为
下一个测试将获取一个新的应用程序上下文 缓存的上下文已经被销毁,取而代之,新的上下文会被缓存
测试属性源
自定义仅用于测试的properties
- 定义1个或多个properties 具有更高的优先级
- 定义需要加载的1个或多个properties的位置 默认情况下将查找[classname].properties
使用Spring进行测试的好处
无需部署到外部容器中测试应用程序功能
- 在你的IDEA内快速运行一切
- 支持持续集成测试
允许在测试环境和生产环境之间重复使用你的配置
应用程序配置逻辑通常被重复使用
基础架构配置与环境相关 数据源 JMS序列
激活测试使用的Profiles
测试类中的@ActiveProfiles
- 定义1个或多个Profiles
- 与该Profile关联的Bean会被实例化
- 还有不关联到任何Profile的Bean
示例:激活2个Profiles - jdbc和dev
使用Java配置的Profiles激活
在@Configuration类或任何它的@Bean方法上使用@Profile
使用注解的Profiles激活
在组件类上使用@Profile
使用数据库测试
也许,你在执行数据测试时遇到过这些问题
- 数据库的“用户名”字段设置为了“唯一(unique)”约束,第1次测试插入数据成功了,第2次就会因为“唯一”约束而失败
- 想要测试更新或删除数据,但表中没有数据,或不知道数据的id
- 想要测试查询某些数据,但表中没有适合用于测试的条件的数据 例如测试分页查询,但表中的数据量太大
使用数据库测试
- 针对SQl数据库的测试很常见
- 对于这类测试,基于内存的数据很实用 无需提前安装
- 常见需求:在测试之前填充DB 使用@Sql注解
@Sql示例
@Sql选项
SQL何时/如何运行?
- executionPhase:在测试方法之前(默认)或之后
- config:控制SQL脚本的选项 如果脚本执行失败了怎么办?FAIL_ON_ERROR, CONTINUE_ON_ERROR, IGNORE_FAILED_DROPS,DEFAULT* SQL语法控制:注释,语句分隔符
总结
测试是任何开发中的重要组成部分
单元测试是对一个类进行隔离测试
- 外部依赖项应该最小化
- 考虑创建Stubs或Mocks进行单元测试
- 你不需要Spring进行单元测试
集成测试则测试多个单元共同工作的互动
- Spring提供了良好的集成测试支持
- 不同的测试与部署配置使用不同的Profiles
- 内置支持数据库测试
JUnit 4与JUnit 5的区别
- JUnit 5增强了JUnit框架 在JUnit 4的基础上增加了新的功能
- JUnit 5支持多种扩展 解决了JUnit 4的“单一运行者限制”
JUnit 5断言(断言:本次测试结果必定如此)
使用断言 -- 添加单元测试依赖
使用断言 -- 静态导入Assertions类所有静态成员
Assertions类中可使用的断言方法(均重载多次)
新的断言
新的断言介绍
- assertThrows(<exception.class>,<Lambda表达式>)
- assertTimeout(<duration>,<Lambda表达式>)
- assertAll(<description>,<multiple-assertions>)
改进设想
使用Lambda表达式 要测试的代码
assertThrows(..)与assertTimeout(..)
假设
断言与假设区别
- 当测试不通过时,断言将失败(failed),而假设将终止(aborted)
- 当失败时,会显示预期的信息(例如断言的期望值/期望效果、实际值/实际效果),当终止,则结束
- 当你明确测试结果,可以使用断言,当你并不能肯定测试结果是哪一种,可以使用假设
Spring JDBC简介
模板设计模式
广泛使用且实用的模式: http://en.wikipedia.org/wiki/Template_method_pattern
定义算法的轮廓或骨架
- 具体细节留待以后具体的实现
- 隐藏大量的模板代码
Spring提供了许多模板类
- JdbcTemplate,JmsTemplate
- RestTemplate,WebServiceTemplate....
- 大部分隐藏了低级资源管理
Spring的JdbcTemplate
大大简化了JDBC API的使用
- 消除重复的模板代码
- 减轻引起错误的常见原因
- 正确处理SQLExceptions
在不牺牲性能的情况下 提供对标准JDBC结构的完全访问
JdbcTemplate简介
JdbcTemplate内部的回调图
创建一个JdbcTemplate
需要一个 DataSource: JdbcTemplate template = new JdbcTemplate(dataSource);
创建一个模板并重复使用它
- 不要为每个线程都创建
- 构建之后线程安全
使用
- 任何需要JDBC的时候
- 在实用或测试代码中
- 清理混乱的遗留代码
JdbcTemplate的使用
实现一个基于JDBC的存储库
使用JdbcTemplate查询
JdbcTemplate能查询:
- 简单类型(int,long,String,Date,...)
- Generic Maps
- Domain Objects
查询简单的Java类型
无绑定变量的查询
绑定变量的查询
能使用绑定变量进行查询 注意变量参数列表的使用
处理结果集
一般查询
JdbcTemplate能将ResultSet中的每一行作为一个Map返回
- 当查询的预期结果只有1行时 使用queryForMap(..)
- 当查询的预期结果有多行时 使用queryForList(..)
实用与ad hoc数据、测试报告 获取到的数据不需要映射到Java对象
查询Generic Maps(1)
查询1个结果
返回
查询Generix Maps(2)
查询多行数据
返回
查询Domain Object
通常情况下,将关联数据映射到domain objects非常实用 例如将ResultSet 映射到 Account
Spring的JdbcTemplate使用回调方法进行支持
你可能使用OPM
- 需要在JdbcTemplate查询和JPA(或类似的)映射之间做出决定
- 有些表可能难以使用JPA映射
RowMapper用于映射
Spring提供了一个RowMapper接口,用于将一个ResultSet的某1行映射到1个对象上
- 可用于单行和多行查询
- 参数化定义其返回类型
查询Domain Objects
使用JdbcTemplate查询1行数据
查询Domain Object(2)
查询多行数据
ResultSetExtractor
Spring提供了一个ResultSetExtractor接口,用于一次性处理整个ResultSet
- 你负责迭代ResultSet
- 示例:将整个ResultSet映射到一个对象上
使用ResultSetExtractor
回调接口总结
- RowMapper 当ResultSet的每一行都映射到Domain Object时的最佳选择
- ResultSetExtractor 当一个ResultSet的多行映射到单个对象时的最佳选择
- RowCallbackHandler 另一个写到其他目的地的处理程序
异常处理与Spring
- Checked Exceptions 强制开发者处理错误 但是如果你不能处理,则声明它。 缺点:中间方法必须声明下面所有方法的异常 (紧耦合的一种形式)
- Unchecked Exceptions 可以抛出调用层次结构的最佳处理位置。 优点:中间方法并不知道它的存在,在企业级程序中,使用它更好一些。 Spring抛出的一直是Runtime(unchecked)Exceptions
数据访问Exceptions
SQLException
- 过于笼统——每一个数据库错误都有一个异常
- 调用类“知道”你正在使用JDBC
- 紧耦合
Spring提供了DataAccessException体系
- 隐藏你是否在使用JPA、Hibernate、JDBC...
- 实际上是一个异常的体系 对于各种情况而言不只是一种异常
- 在所有支持的数据访问技术中保持一致
- Unchecked
Spring的数据访问Exceptions
总结
- JDBC很实用 但是直接使用JDBC API既繁琐又容易出错
- JdbcTemplate简化了 了一致性: DRY原则隐藏了大部分的JDBC 读取数据的多种选择
- SQLExceptions通常不能在抛出的位置被处理 : 不应该是checked Exceptions Spring提供了DataAccessException来取代它
使用Spring进行事务管理
为什么使用事务
什么是事务
作为单一、不可分割的行动而进行的任务集
- Atomic(原子性):每个工作单元都是一个全部或全无的操作
- Consistent(一致性):永不违背数据库完整性约束
- Lsolated(隔离性):彼此之间隔离事务
- Durable(持久性):提交的数据改变是永久性的
Java事务管理
局部和全局的事务管理
Java事务API——不同资源的不同API(难以使用)
Java事务管理的问题
- 不同本地资源的多个API
- 程序化事务的划分 通常在业务层中进行,但我们不希望数据访问代码在业务层中(关注点分离) 通常重复(横切关注)
- 正交关注 事务划分应独立于事务实现
Spring事务管理
- Spring将事务划分与事务实现分离 通过AOP声明式表达的划分(也可以用程序化方法) PlatformTransactionManager提取隐藏了实现细节(有多种可用的实现)
- Spring对全局或本地使用相同的API 从本地到全局的变化不大(只改变了事务管理器)
有两个步骤
- 声明一个PlatformTransactionManager类型的Bean
- 声明事务的方法 使用注解,程序化 能混合并且匹配
PlatformTransactionManager的实现
Spring的PlatformTransactionManager是提取的基础接口
有多种可用的实现
- DataSourceTransactionManager
- JmsTransactionManager
- JpaTransactionManager
- JtaTransactionManages
- WebLogicJtaTransactionManages
- WebSphereUowTransactionManages
- ....
部署事务管理器
创建所需的实现 就像其它的Spring bean一样(适当配置它)
例:DataSource的管理器
访问JTA事务管理器
对容器管理的DataSource使用JNDI查找
@Transactional配置
声明式事务管理
目标业务被包裹在代理中 使用“around”类型的Advice
代理引用注入调用器
@Transaction:中的实现
代理实现了一下行为
- 在进入该方法之前,事务已经开始
- 在方法结束时提交
- 如果方法抛出RuntimeException则回滚 (a.默认行为 b.可以被覆盖 c.Checked exceptions不会导致回滚)
Transaction绑定当前线程
Transaction上下文绑定了当前线程 持有底层的JDBC连接 Hibernate会话,JTA(Java EE)工作方式相似
在@Transactional方法中使用JdbcTemplate 自动使用那个连接
手动访问方式:
@Transaction 在类级别
适用于接口声明的所有方法
@Transaction 在类和方法级别
结合类和方法级别
Java的@Transaction
Java也有一个注解:javax.transaction.Transactional
Spring也支持:Spring的@Transactional
事务的传播
例:
如果ClientServiceImpl调用AccountServiceImpl会发生什么
事务传播分析图
事务的传播级别与它们的行为
例:REQUIRED
默认值
在当前事务中执行,如果当前没有事务,则创建一个新事务
REQUIRES_NEW
创建一个新的事务,如果当前已存在事务,则暂停当前事务
传播规则由代理强制执行
下列,传播规则并没有被应用,因为没有通过代理来调用
事务的隔离级别
4种隔离级别
事务的隔离级别是保证事务隔离性的解决方案
由低到高:
Read uncommitted、Read committed、Repeatable read、Serializable
Read uncommitted 读未提交
一个事务可以读取到另一个事务未提交的操作的结果。该级别不保证任何的隔离性。该隔离级别可能出现脏读、不可重复度、幻读问题。
- 脏读:一个事务使用了另一个事务未提交数据
- 不可重复度:指在一个事务内,多次读同一数据,得到的结果不同
- 幻读:一个事务读取全表数据时,读取到另一个事务向表中新增、删除操作提交的结果
Read committed 读已提交
一个事务仅能读取到另一个事务已提交的数据 ,可以保证部分隔离性,可以防止脏读问题,但是具有不可重复读和幻读问题。
Repeatable read 可重复读
该级别下,一个事务无法读取另一个后开启的事务已提交的操作结果
修改隔离级别