Java学习指南
  • Java 编程的逻辑
  • Java进阶
  • Java FrameWorks
  • 了解 USB Type-A,B,C 三大标准接口
  • 深入浅出DDD
  • 重构:改善既有代码的设计
  • 面试大纲
  • 云原生
    • 什么是无服务器(what is serverless)?
  • 博客
    • 深入分析Log4j 漏洞
  • 博客
    • Serverless之快速搭建Spring Boot应用
  • 博客
    • 使用 Prometheus + Grafana + Spring Boot Actuator 监控应用
  • 博客
    • 使用 Prometheus + Grafana 监控 MySQL
  • 博客
    • 使用Github Actions + Docker 部署Spring Boot应用
  • 博客
    • Redis分布式锁之Redisson的原理和实践
  • 博客
    • 数据库中的树结构应该怎样去设计
  • 学习&成长
    • 如何成为技术大牛
  • 开发工具
    • Git Commit Message Guidelines
  • 开发工具
    • git命名大全
  • 开发工具
    • Gradle vs Maven Comparison
  • 开发工具
    • Swagger2常用注解及其说明
  • 开发工具
    • 简明 VIM 练级攻略
  • 微服务
    • 十大微服务设计模式和原则
  • 微服务
    • 微服务下的身份认证和令牌管理
  • 微服务
    • 微服务坏味道之循环依赖
  • 设计模式
    • 设计模式 - JDK中的设计模式
  • 设计模式
    • 设计模式 - Java三种代理模式
  • 设计模式
    • 设计模式 - 六大设计原则
  • 设计模式
    • 设计模式 - 单例模式
  • 设计模式
    • 设计模式 - 命名模式
  • 设计模式
    • 设计模式 - 备忘录模式
  • 设计模式
    • 设计模式 - 概览
  • 设计模式
    • 设计模式 - 没用的设计模式
  • 质量&效率
    • Homebrew 替换国内镜像源
  • 质量&效率
    • 工作中如何做好技术积累
  • Java FrameWorks
    • Logback
      • 自定义 logback 日志过滤器
  • Java FrameWorks
    • Mybatis
      • MyBatis(十三) - 整合Spring
  • Java FrameWorks
    • Mybatis
      • MyBatis(十二) - 一些API
  • Java FrameWorks
    • Mybatis
      • Mybatis(一) - 概述
  • Java FrameWorks
    • Mybatis
      • Mybatis(七) - 结果集的封装与映射
  • Java FrameWorks
    • Mybatis
      • Mybatis(三) - mapper.xml及其加载机制
  • Java FrameWorks
    • Mybatis
      • Mybatis(九) - 事务
  • Java FrameWorks
    • Mybatis
      • Mybatis(二) - 全局配置文件及其加载机制
  • Java FrameWorks
    • Mybatis
      • Mybatis(五) - SqlSession执行流程
  • Java FrameWorks
    • Mybatis
      • Mybatis(八) - 缓存
  • Java FrameWorks
    • Mybatis
      • Mybatis(六) - 动态SQL的参数绑定与执行
  • Java FrameWorks
    • Mybatis
      • Mybatis(十) - 插件
  • Java FrameWorks
    • Mybatis
      • Mybatis(十一) - 日志
  • Java FrameWorks
    • Mybatis
      • Mybatis(四) - Mapper接口解析
  • Java FrameWorks
    • Netty
      • Netty 可靠性分析
  • Java FrameWorks
    • Netty
      • Netty - Netty 线程模型
  • Java FrameWorks
    • Netty
      • Netty堆外内存泄露排查盛宴
  • Java FrameWorks
    • Netty
      • Netty高级 - 高性能之道
  • Java FrameWorks
    • Shiro
      • Shiro + JWT + Spring Boot Restful 简易教程
  • Java FrameWorks
    • Shiro
      • 非常详尽的 Shiro 架构解析!
  • Java FrameWorks
    • Spring
      • Spring AOP 使用介绍,从前世到今生
  • Java FrameWorks
    • Spring
      • Spring AOP 源码解析
  • Java FrameWorks
    • Spring
      • Spring Event 实现原理
  • Java FrameWorks
    • Spring
      • Spring Events
  • Java FrameWorks
    • Spring
      • Spring IOC容器源码分析
  • Java FrameWorks
    • Spring
      • Spring Integration简介
  • Java FrameWorks
    • Spring
      • Spring MVC 框架中拦截器 Interceptor 的使用方法
  • Java FrameWorks
    • Spring
      • Spring bean 解析、注册、实例化流程源码剖析
  • Java FrameWorks
    • Spring
      • Spring validation中@NotNull、@NotEmpty、@NotBlank的区别
  • Java FrameWorks
    • Spring
      • Spring 如何解决循环依赖?
  • Java FrameWorks
    • Spring
      • Spring 异步实现原理与实战分享
  • Java FrameWorks
    • Spring
      • Spring中的“for update”问题
  • Java FrameWorks
    • Spring
      • Spring中的设计模式
  • Java FrameWorks
    • Spring
      • Spring事务失效的 8 大原因
  • Java FrameWorks
    • Spring
      • Spring事务管理详解
  • Java FrameWorks
    • Spring
      • Spring计时器StopWatch使用
  • Java FrameWorks
    • Spring
      • 详述 Spring MVC 框架中拦截器 Interceptor 的使用方法
  • Java FrameWorks
    • Spring
      • 透彻的掌握 Spring 中@transactional 的使用
  • Java
    • Java IO&NIO&AIO
      • Java IO - BIO 详解
  • Java
    • Java IO&NIO&AIO
      • Java NIO - IO多路复用详解
  • Java
    • Java IO&NIO&AIO
      • Java N(A)IO - Netty
  • Java
    • Java IO&NIO&AIO
      • Java IO - Unix IO模型
  • Java
    • Java IO&NIO&AIO
      • Java IO - 分类
  • Java
    • Java IO&NIO&AIO
      • Java NIO - 基础详解
  • Java
    • Java IO&NIO&AIO
      • Java IO - 常见类使用
  • Java
    • Java IO&NIO&AIO
      • Java AIO - 异步IO详解
  • Java
    • Java IO&NIO&AIO
      • Java IO概述
  • Java
    • Java IO&NIO&AIO
      • Java IO - 设计模式
  • Java
    • Java IO&NIO&AIO
      • Java NIO - 零拷贝实现
  • Java
    • Java JVM
      • JVM 优化经验总结
  • Java
    • Java JVM
      • JVM 内存结构
  • Java
    • Java JVM
      • JVM参数设置
  • Java
    • Java JVM
      • Java 内存模型
  • Java
    • Java JVM
      • 从实际案例聊聊Java应用的GC优化
  • Java
    • Java JVM
      • Java 垃圾回收器G1详解
  • Java
    • Java JVM
      • 垃圾回收器Shenandoah GC详解
  • Java
    • Java JVM
      • 垃圾回收器ZGC详解
  • Java
    • Java JVM
      • 垃圾回收基础
  • Java
    • Java JVM
      • 如何优化Java GC
  • Java
    • Java JVM
      • 类加载机制
  • Java
    • Java JVM
      • 类字节码详解
  • Java
    • Java 基础
      • Java hashCode() 和 equals()
  • Java
    • Java 基础
      • Java 基础 - Java native方法以及JNI实践
  • Java
    • Java 基础
      • Java serialVersionUID 有什么作用?
  • Java
    • Java 基础
      • Java 泛型的类型擦除
  • Java
    • Java 基础
      • Java 基础 - Unsafe类解析
  • Java
    • Java 基础
      • Difference Between Statement and PreparedStatement
  • Java
    • Java 基础
      • Java 基础 - SPI机制详解
  • Java
    • Java 基础
      • Java 基础 - final
  • Java
    • Java 基础
      • Java中static关键字详解
  • Java
    • Java 基础
      • 为什么说Java中只有值传递?
  • Java
    • Java 基础
      • Java 基础 - 即时编译器原理解析及实践
  • Java
    • Java 基础
      • Java 基础 - 反射
  • Java
    • Java 基础
      • Java多态的面试题
  • Java
    • Java 基础
      • Java 基础 - 异常机制详解
  • Java
    • Java 基础
      • 为什么要有抽象类?
  • Java
    • Java 基础
      • 接口的本质
  • Java
    • Java 基础
      • Java 基础 - 枚举
  • Java
    • Java 基础
      • Java 基础 - 泛型机制详解
  • Java
    • Java 基础
      • Java 基础 - 注解机制详解
  • Java
    • Java 基础
      • 为什么 String hashCode 方法选择数字31作为乘子
  • Java
    • Java 并发
      • Java 并发 - 14个Java并发容器
  • Java
    • Java 并发
      • Java 并发 - AQS
  • Java
    • Java 并发
      • Java 并发 - BlockingQueue
  • Java
    • Java 并发
      • Java 并发 - CAS
  • Java
    • Java 并发
      • Java 并发 - Condition接口
  • Java
    • Java 并发
      • Java 并发 - CopyOnWriteArrayList
  • Java
    • Java 并发
      • Java 并发 - CountDownLatch、CyclicBarrier和Phaser对比
  • Java
    • Java 并发
      • Java 并发 - Fork&Join框架
  • Java
    • Java 并发
      • Java 并发 - Java CompletableFuture 详解
  • Java
    • Java 并发
      • Java 并发 - Java 线程池
  • Java
    • Java 并发
      • Java 并发 - Lock接口
  • Java
    • Java 并发
      • Java 并发 - ReentrantLock
  • Java
    • Java 并发
      • Java 并发 - ReentrantReadWriteLock
  • Java
    • Java 并发
      • Java 并发 - Synchronized
  • Java
    • Java 并发
      • Java 并发 - ThreadLocal 内存泄漏问题
  • Java
    • Java 并发
      • Java 并发 - ThreadLocal
  • Java
    • Java 并发
      • Java 并发 - Volatile
  • Java
    • Java 并发
      • Java 并发 - 从ReentrantLock的实现看AQS的原理及应用
  • Java
    • Java 并发
      • Java 并发 - 公平锁和非公平锁
  • Java
    • Java 并发
      • Java 并发 - 内存模型
  • Java
    • Java 并发
      • Java 并发 - 原子类
  • Java
    • Java 并发
      • Java 并发 - 如何确保三个线程顺序执行?
  • Java
    • Java 并发
      • Java 并发 - 锁
  • Java
    • Java 的新特性
      • Java 10 新特性概述
  • Java
    • Java 的新特性
      • Java 11 新特性概述
  • Java
    • Java 的新特性
      • Java 12 新特性概述
  • Java
    • Java 的新特性
      • Java 13 新特性概述
  • Java
    • Java 的新特性
      • Java 14 新特性概述
  • Java
    • Java 的新特性
      • Java 15 新特性概述
  • Java
    • Java 的新特性
      • Java 8的新特性
  • Java
    • Java 的新特性
      • Java 9 新特性概述
  • Java
    • Java 调试排错
      • 调试排错 - Java Debug Interface(JDI)详解
  • Java
    • Java 调试排错
      • 调试排错 - CPU 100% 排查优化实践
  • Java
    • Java 调试排错
      • 调试排错 - Java Heap Dump分析
  • Java
    • Java 调试排错
      • 调试排错 - Java Thread Dump分析
  • Java
    • Java 调试排错
      • 调试排错 - Java动态调试技术原理
  • Java
    • Java 调试排错
      • 调试排错 - Java应用在线调试Arthas
  • Java
    • Java 调试排错
      • 调试排错 - Java问题排查:工具单
  • Java
    • Java 调试排错
      • 调试排错 - 内存溢出与内存泄漏
  • Java
    • Java 调试排错
      • 调试排错 - 在线分析GC日志的网站GCeasy
  • Java
    • Java 调试排错
      • 调试排错 - 常见的GC问题分析与解决
  • Java
    • Java 集合
      • Java 集合 - ArrayList
  • Java
    • Java 集合
      • Java 集合 - HashMap 和 ConcurrentHashMap
  • Java
    • Java 集合
      • Java 集合 - HashMap的死循环问题
  • Java
    • Java 集合
      • Java 集合 - LinkedHashSet&Map
  • Java
    • Java 集合
      • Java 集合 - LinkedList
  • Java
    • Java 集合
      • Java 集合 - PriorityQueue
  • Java
    • Java 集合
      • Java 集合 - Stack & Queue
  • Java
    • Java 集合
      • Java 集合 - TreeSet & TreeMap
  • Java
    • Java 集合
      • Java 集合 - WeakHashMap
  • Java
    • Java 集合
      • Java 集合 - 为什么HashMap的容量是2的幂次方
  • Java
    • Java 集合
      • Java 集合 - 概览
  • Java
    • Java 集合
      • Java 集合 - 高性能队列Disruptor详解
  • 分布式
    • RPC
      • ⭐️RPC - Dubbo&hsf&Spring cloud的区别
  • 分布式
    • RPC
      • ⭐️RPC - Dubbo的架构原理
  • 分布式
    • RPC
      • ⭐️RPC - HSF的原理分析
  • 分布式
    • RPC
      • ⭐️RPC - 你应该知道的RPC原理
  • 分布式
    • RPC
      • ⭐️RPC - 动态代理
  • 分布式
    • RPC
      • 深入理解 RPC 之协议篇
  • 分布式
    • RPC
      • RPC - 序列化和反序列化
  • 分布式
    • RPC
      • ⭐️RPC - 服务注册与发现
  • 分布式
    • RPC
      • RPC - 核心原理
  • 分布式
    • RPC
      • ⭐️RPC - 框架对比
  • 分布式
    • RPC
      • ⭐️RPC - 网络通信
  • 分布式
    • 分布式事务
      • 分布式事务 Seata TCC 模式深度解析
  • 分布式
    • 分布式事务
      • 分布式事务的实现原理
  • 分布式
    • 分布式事务
      • 常用的分布式事务解决方案
  • 分布式
    • 分布式事务
      • 手写实现基于消息队列的分布式事务框架
  • 分布式
    • 分布式算法
      • CAP 定理的含义
  • 分布式
    • 分布式算法
      • Paxos和Raft比较
  • 分布式
    • 分布式算法
      • 分布式一致性与共识算法
  • 分布式
    • 分布式锁
      • ⭐️分布式锁的原理及实现方式
  • 分布式
    • 搜索引擎
      • ElasticSearch与SpringBoot的集成与JPA方法的使用
  • 分布式
    • 搜索引擎
      • 全文搜索引擎 Elasticsearch 入门教程
  • 分布式
    • 搜索引擎
      • 十分钟学会使用 Elasticsearch 优雅搭建自己的搜索系统
  • 分布式
    • 搜索引擎
      • 腾讯万亿级 Elasticsearch 技术解密
  • 分布式
    • 日志系统
      • Grafana Loki 简明教程
  • 分布式
    • 日志系统
      • 分布式系统中如何优雅地追踪日志
  • 分布式
    • 日志系统
      • 如何优雅地记录操作日志?
  • 分布式
    • 日志系统
      • 日志收集组件—Flume、Logstash、Filebeat对比
  • 分布式
    • 日志系统
      • 集中式日志系统 ELK 协议栈详解
  • 分布式
    • 消息队列
      • 消息队列 - Kafka
  • 分布式
    • 消息队列
      • 消息队列 - Kafka、RabbitMQ、RocketMQ等消息中间件的对比
  • 分布式
    • 消息队列
      • 消息队列之 RabbitMQ
  • 分布式
    • 消息队列
      • 消息队列 - 使用docker-compose构建kafka集群
  • 分布式
    • 消息队列
      • 消息队列 - 分布式系统与消息的投递
  • 分布式
    • 消息队列
      • 消息队列 - 如何保证消息的可靠性传输
  • 分布式
    • 消息队列
      • 消息队列 - 如何保证消息的顺序性
  • 分布式
    • 消息队列
      • 消息队列 - 如何保证消息队列的高可用
  • 分布式
    • 消息队列
      • 消息队列 - 消息队列设计精要
  • 分布式
    • 监控系统
      • 深度剖析开源分布式监控CAT
  • 大数据
    • Flink
      • Flink架构与核心组件
  • 微服务
    • Dubbo
      • 基于dubbo的分布式应用中的统一异常处理
  • 微服务
    • Dubbo
      • Vim快捷键
  • 微服务
    • Service Mesh
      • Istio 是什么?
  • 微服务
    • Service Mesh
      • OCTO 2.0:美团基于Service Mesh的服务治理系统详解
  • 微服务
    • Service Mesh
      • Service Mesh是什么?
  • 微服务
    • Service Mesh
      • Spring Cloud向Service Mesh迁移
  • 微服务
    • Service Mesh
      • 数据挖掘算法
  • 微服务
    • Service Mesh
      • Seata Saga 模式
  • 微服务
    • Spring Cloud
      • Seata TCC 模式
  • 微服务
    • Spring Cloud
      • Spring Cloud Config
  • 微服务
    • Spring Cloud
      • Seata AT 模式
  • 微服务
    • Spring Cloud
      • Spring Cloud Gateway
  • 微服务
    • Spring Cloud
      • Spring Cloud OpenFeign 的核心原理
  • 微服务
    • Spring Cloud
      • Seata XA 模式
  • 数据库
    • Database Version Control
      • Liquibase vs. Flyway
  • 数据库
    • Database Version Control
      • Six reasons to version control your database
  • 数据库
    • MySQL
      • How Sharding Works
  • 数据库
    • MySQL
      • MySQL InnoDB中各种SQL语句加锁分析
  • 数据库
    • MySQL
      • MySQL 事务隔离级别和锁
  • 数据库
    • MySQL
      • MySQL 索引性能分析概要
  • 数据库
    • MySQL
      • MySQL 索引设计概要
  • 数据库
    • MySQL
      • MySQL出现Waiting for table metadata lock的原因以及解决方法
  • 数据库
    • MySQL
      • MySQL的Limit性能问题
  • 数据库
    • MySQL
      • MySQL索引优化explain
  • 数据库
    • MySQL
      • MySQL索引背后的数据结构及算法原理
  • 数据库
    • MySQL
      • MySQL行转列、列转行问题
  • 数据库
    • MySQL
      • 一条SQL更新语句是如何执行的?
  • 数据库
    • MySQL
      • 一条SQL查询语句是如何执行的?
  • 数据库
    • MySQL
      • 为什么 MySQL 使用 B+ 树
  • 数据库
    • MySQL
      • 为什么 MySQL 的自增主键不单调也不连续
  • 数据库
    • MySQL
      • 为什么我的MySQL会“抖”一下?
  • 数据库
    • MySQL
      • 为什么数据库不应该使用外键
  • 数据库
    • MySQL
      • 为什么数据库会丢失数据
  • 数据库
    • MySQL
      • 事务的可重复读的能力是怎么实现的?
  • 数据库
    • MySQL
      • 大众点评订单系统分库分表实践
  • 数据库
    • MySQL
      • 如何保证缓存与数据库双写时的数据一致性?
  • 数据库
    • MySQL
      • 浅谈数据库并发控制 - 锁和 MVCC
  • 数据库
    • MySQL
      • 深入浅出MySQL 中事务的实现
  • 数据库
    • MySQL
      • 浅入浅出MySQL 和 InnoDB
  • 数据库
    • PostgreSQL
      • PostgreSQL upsert功能(insert on conflict do)的用法
  • 数据库
    • Redis
      • Redis GEO & 实现原理深度分析
  • 数据库
    • Redis
      • Redis 和 I/O 多路复用
  • 数据库
    • Redis
      • Redis分布式锁
  • 数据库
    • Redis
      • Redis实现分布式锁中的“坑”
  • 数据库
    • Redis
      • Redis总结
  • 数据库
    • Redis
      • 史上最全Redis高可用技术解决方案大全
  • 数据库
    • Redis
      • Redlock:Redis分布式锁最牛逼的实现
  • 数据库
    • Redis
      • 为什么 Redis 选择单线程模型
  • 数据库
    • TiDB
      • 新一代数据库TiDB在美团的实践
  • 数据库
    • 数据仓库
      • 实时数仓在有赞的实践
  • 数据库
    • 数据库原理
      • OLTP与OLAP的关系是什么?
  • 数据库
    • 数据库原理
      • 为什么 OLAP 需要列式存储
  • 系统设计
    • DDD
      • Domain Primitive
  • 系统设计
    • DDD
      • Repository模式
  • 系统设计
    • DDD
      • 应用架构
  • 系统设计
    • DDD
      • 聊聊如何避免写流水账代码
  • 系统设计
    • DDD
      • 领域层设计规范
  • 系统设计
    • DDD
      • 从三明治到六边形
  • 系统设计
    • DDD
      • 阿里盒马领域驱动设计实践
  • 系统设计
    • DDD
      • 领域驱动设计(DDD)编码实践
  • 系统设计
    • DDD
      • 领域驱动设计在互联网业务开发中的实践
  • 系统设计
    • 基础架构
      • 容错,高可用和灾备
  • 系统设计
    • 数据聚合
      • GraphQL及元数据驱动架构在后端BFF中的实践
  • 系统设计
    • 数据聚合
      • 高效研发-闲鱼在数据聚合上的探索与实践
  • 系统设计
    • 服务安全
      • JSON Web Token 入门教程
  • 系统设计
    • 服务安全
      • 你还在用JWT做身份认证嘛?
  • 系统设计
    • 服务安全
      • 凭证(Credentials)
  • 系统设计
    • 服务安全
      • 授权(Authorization)
  • 系统设计
    • 服务安全
      • 理解OAuth2.0
  • 系统设计
    • 服务安全
      • 认证(Authentication)
  • 系统设计
    • 架构案例
      • 微信 Android 客户端架构演进之路
  • 系统设计
    • 高可用架构
      • 业务高可用的保障:异地多活架构
  • 计算机基础
    • 字符编码
      • Base64原理解析
  • 计算机基础
    • 字符编码
      • 字符编码笔记:ASCII,Unicode 和 UTF-8
  • 计算机基础
    • 操作系统
      • 为什么 CPU 访问硬盘很慢
  • 计算机基础
    • 操作系统
      • 为什么 HTTPS 需要 7 次握手以及 9 倍时延
  • 计算机基础
    • 操作系统
      • 为什么 Linux 默认页大小是 4KB
  • 计算机基础
    • 操作系统
      • 磁盘IO那些事
  • 计算机基础
    • 操作系统
      • 虚拟机的3种网络模式
  • 计算机基础
    • 服务器
      • mac终端bash、zsh、oh-my-zsh最实用教程
  • 计算机基础
    • 服务器
      • Nginx强制跳转Https
  • 计算机基础
    • 服务器
      • curl 的用法指南
  • 计算机基础
    • 网络安全
      • 如何设计一个安全的对外接口?
  • 计算机基础
    • 网络安全
      • 浅谈常见的七种加密算法及实现
  • 计算机基础
    • 网络编程
      • MQTT - The Standard for IoT Messaging
  • 计算机基础
    • 网络编程
      • 两万字长文 50+ 张趣图带你领悟网络编程的内功心法
  • 计算机基础
    • 网络编程
      • 为什么 TCP 协议有 TIME_WAIT 状态
  • 计算机基础
    • 网络编程
      • 为什么 TCP 协议有性能问题
  • 计算机基础
    • 网络编程
      • 为什么 TCP 协议有粘包问题
  • 计算机基础
    • 网络编程
      • 为什么 TCP 建立连接需要三次握手
  • 计算机基础
    • 网络编程
      • 为什么 TCP/IP 协议会拆分数据
  • 计算机基础
    • 网络编程
      • 使用 OAuth 2 和 JWT 为微服务提供安全保障
  • 计算机基础
    • 网络编程
      • 四种常见的 POST 提交数据方式
  • 计算机基础
    • 网络编程
      • 有赞TCP网络编程最佳实践
  • 计算机基础
    • 网络编程
      • 看完这篇HTTP,跟面试官扯皮就没问题了
  • 计算机基础
    • 网络编程
      • 详细解析 HTTP 与 HTTPS 的区别
  • 质量&效率
    • 快捷键
      • Idea快捷键(Mac版)
  • 质量&效率
    • 快捷键
      • Shell快捷键
  • 质量&效率
    • 快捷键
      • conduit
  • 质量&效率
    • 敏捷开发
      • Scrum的3种角色
  • 质量&效率
    • 敏捷开发
      • Scrum的4种会议
  • 质量&效率
    • 敏捷开发
      • ThoughtWorks的敏捷开发
  • 质量&效率
    • 敏捷开发
      • 敏捷开发入门教程
  • 运维&测试
    • Docker
      • Docker (容器) 的原理
  • 运维&测试
    • Docker
      • Docker Compose:链接外部容器的几种方式
  • 运维&测试
    • Docker
      • Docker 入门教程
  • 运维&测试
    • Docker
      • Docker 核心技术与实现原理
  • 运维&测试
    • Docker
      • Dockerfile 最佳实践
  • 运维&测试
    • Docker
      • Docker开启Remote API 访问 2375端口
  • 运维&测试
    • Docker
      • Watchtower - 自动更新 Docker 镜像与容器
  • 运维&测试
    • Kubernetes
      • Kubernetes 介绍
  • 运维&测试
    • Kubernetes
      • Kubernetes 在有赞的实践
  • 运维&测试
    • Kubernetes
      • Kubernetes 学习路径
  • 运维&测试
    • Kubernetes
      • Kubernetes如何改变美团的云基础设施?
  • 运维&测试
    • Kubernetes
      • Kubernetes的三种外部访问方式:NodePort、LoadBalancer 和 Ingress
  • 运维&测试
    • Kubernetes
      • 谈 Kubernetes 的架构设计与实现原理
  • 运维&测试
    • 压测
      • 全链路压测平台(Quake)在美团中的实践
  • 运维&测试
    • 测试
      • Cpress - JavaScript End to End Testing Framework
  • 运维&测试
    • 测试
      • 代码覆盖率-JaCoCo
  • 运维&测试
    • 测试
      • 浅谈代码覆盖率
  • 运维&测试
    • 测试
      • 测试中 Fakes、Mocks 以及 Stubs 概念明晰
  • Java FrameWorks
    • Spring
      • Spring AOP
        • Spring AOP中的Bean是如何被AOP代理的
  • Java FrameWorks
    • Spring
      • Spring AOP
        • Spring AOP原生动态代理和Cglib动态代理
  • Java FrameWorks
    • Spring
      • Spring AOP
        • Spring AOP实现方式(xml&注解)
  • Java FrameWorks
    • Spring
      • Spring AOP
        • Spring AOP是如何收集切面类并封装的
  • Java FrameWorks
    • Spring
      • Spring AOP
        • Spring AOP概述
  • Java FrameWorks
    • Spring
      • Spring AOP
        • Spring AOP的底层核心后置处理器
  • Java FrameWorks
    • Spring
      • Spring AOP
        • Spring AOP的延伸知识
  • Java FrameWorks
    • Spring
      • Spring Boot
        • Spring Boot - IOC(一)
  • Java FrameWorks
    • Spring
      • Spring Boot
        • Spring Boot - IOC(三)
  • Java FrameWorks
    • Spring
      • Spring Boot
        • Spring Boot - IOC(二)
  • Java FrameWorks
    • Spring
      • Spring Boot
        • Spring Boot - IOC(五)
  • Java FrameWorks
    • Spring
      • Spring Boot
        • Spring Boot - IOC(四) - 循环依赖与解决方案
  • Java FrameWorks
    • Spring
      • Spring Boot
        • Spring Boot - 启动引导
  • Java FrameWorks
    • Spring
      • Spring Boot
        • Spring Boot JarLauncher
  • Java FrameWorks
    • Spring
      • Spring Boot
        • Spring Boot Web Mvc 自动装配
  • Java FrameWorks
    • Spring
      • Spring Boot
        • Spring Boot 使用ApplicationListener监听器
  • Java FrameWorks
    • Spring
      • Spring Boot
        • Spring Boot 声明式事务
  • Java FrameWorks
    • Spring
      • Spring Boot
        • Spring Boot 嵌入式容器
  • Java FrameWorks
    • Spring
      • Spring Boot
        • Spring Boot引起的“堆外内存泄漏”排查及经验总结
  • Java FrameWorks
    • Spring
      • Spring Boot
        • Spring Boot的启动流程
  • Java FrameWorks
    • Spring
      • Spring Boot
        • Spring Boot自动化配置源码分析
  • Java FrameWorks
    • Spring
      • Spring Boot
        • 如何自定义Spring Boot Starter?
  • Java FrameWorks
    • Spring
      • Spring IOC
        • IOC - 模块装配和条件装配
  • Java FrameWorks
    • Spring
      • Spring IOC
        • IOC - 配置源(xml,注解)
  • Java FrameWorks
    • Spring
      • Spring IOC
        • Spring Environment
  • Java FrameWorks
    • Spring
      • Spring IOC
        • Spring ApplicationContext
  • Java FrameWorks
    • Spring
      • Spring IOC
        • Spring BeanDefinition
  • Java FrameWorks
    • Spring
      • Spring IOC
        • Spring BeanFactory
  • Java FrameWorks
    • Spring
      • Spring IOC
        • Spring BeanFactoryPostProcessor
  • Java FrameWorks
    • Spring
      • Spring IOC
        • Spring BeanPostProcessor
  • Java FrameWorks
    • Spring
      • Spring IOC
        • Spring Bean的生命周期(一) - 概述
  • Java FrameWorks
    • Spring
      • Spring IOC
        • Spring Bean的生命周期(三) - 实例化阶段
  • Java FrameWorks
    • Spring
      • Spring IOC
        • Spring Bean的生命周期(二) - BeanDefinition
  • Java FrameWorks
    • Spring
      • Spring IOC
        • Spring Bean的生命周期(五) - 销毁阶段
  • Java FrameWorks
    • Spring
      • Spring IOC
        • Spring Bean的生命周期(四) - 初始化阶段
  • Java FrameWorks
    • Spring
      • Spring IOC
        • Spring ComponentScan
  • Java FrameWorks
    • Spring
      • Spring IOC
        • Spring Events
  • Java FrameWorks
    • Spring
      • Spring IOC
        • Spring IOC 基础篇
  • Java FrameWorks
    • Spring
      • Spring IOC
        • Spring IOC 总结
  • Java FrameWorks
    • Spring
      • Spring IOC
        • Spring IOC 进阶篇
  • Java FrameWorks
    • Spring
      • Spring IOC
        • Spring IOC容器的生命周期
  • Java FrameWorks
    • Spring
      • Spring IOC
        • Spring Resource
  • Java FrameWorks
    • Spring
      • Spring MVC
        • DispatcherServlet的初始化原理
  • Java FrameWorks
    • Spring
      • Spring MVC
        • DispatcherServlet的核心工作原理
  • Java FrameWorks
    • Spring
      • Spring MVC
        • WebMvc的架构设计与组件功能解析
  • Java FrameWorks
    • Spring
      • Spring Security
        • Spring Boot 2 + Spring Security 5 + JWT 的单页应用 Restful 解决方案
  • Java FrameWorks
    • Spring
      • Spring Security
        • Spring Security Oauth
  • Java FrameWorks
    • Spring
      • Spring Security
        • Spring Security
  • Java FrameWorks
    • Spring
      • Spring WebFlux
        • DispatcherHandler的工作原理(传统方式)
  • Java FrameWorks
    • Spring
      • Spring WebFlux
        • DispatcherHandler的工作原理(函数式端点)
  • Java FrameWorks
    • Spring
      • Spring WebFlux
        • WebFlux的自动装配
  • Java FrameWorks
    • Spring
      • Spring WebFlux
        • 快速了解响应式编程与Reactive
  • Java FrameWorks
    • Spring
      • Spring WebFlux
        • 快速使用WebFlux
  • 分布式
    • 协调服务
      • Zookeeper
        • Zookeeper - 客户端之 Curator
  • 分布式
    • 协调服务
      • Zookeeper
        • 详解分布式协调服务 ZooKeeper
  • 分布式
    • 协调服务
      • etcd
        • 高可用分布式存储 etcd 的实现原理
  • 数据库
    • Database Version Control
      • Flyway
        • Database Migrations with Flyway
  • 数据库
    • Database Version Control
      • Flyway
        • How Flyway works
  • 数据库
    • Database Version Control
      • Flyway
        • Rolling Back Migrations with Flyway
  • 数据库
    • Database Version Control
      • Flyway
        • The meaning of the concept of checksums
  • 数据库
    • Database Version Control
      • Liquibase
        • Introduction to Liquibase Rollback
  • 数据库
    • Database Version Control
      • Liquibase
        • LiquiBase中文学习指南
  • 数据库
    • Database Version Control
      • Liquibase
        • Use Liquibase to Safely Evolve Your Database Schema
  • 系统设计
    • 流量控制
      • RateLimiter
        • Guava Rate Limiter实现分析
  • 系统设计
    • 流量控制
      • Sentinel
        • Sentinel 与 Hystrix 的对比
  • 系统设计
    • 流量控制
      • Sentinel
        • Sentinel工作主流程
  • 系统设计
    • 流量控制
      • 算法
        • 分布式服务限流实战
  • 系统设计
    • 解决方案
      • 秒杀系统
        • 如何设计一个秒杀系统
  • 系统设计
    • 解决方案
      • 红包系统
        • 微信高并发资金交易系统设计方案--百亿红包背后的技术支撑
  • 计算机基础
    • 数据结构与算法
      • 其他相关
        • 什么是预排序遍历树算法(MPTT,Modified Preorder Tree Traversal)
  • 计算机基础
    • 数据结构与算法
      • 其他相关
        • 加密算法
  • 计算机基础
    • 数据结构与算法
      • 其他相关
        • 推荐系统算法
  • 计算机基础
    • 数据结构与算法
      • 其他相关
        • linkerd
  • 计算机基础
    • 数据结构与算法
      • 其他相关
        • 查找算法
  • 计算机基础
    • 数据结构与算法
      • 其他相关
        • 缓存淘汰算法中的LRU和LFU
  • 计算机基础
    • 数据结构与算法
      • 其他相关
        • 负载均衡算法
  • 计算机基础
    • 数据结构与算法
      • 分布式算法
        • 分布式算法 - Paxos算法
  • 计算机基础
    • 数据结构与算法
      • 分布式算法
        • 分布式算法 - Raft算法
  • 计算机基础
    • 数据结构与算法
      • 分布式算法
        • 分布式算法 - Snowflake算法
  • 计算机基础
    • 数据结构与算法
      • 分布式算法
        • 分布式算法 - ZAB算法
  • 计算机基础
    • 数据结构与算法
      • 分布式算法
        • 分布式算法 - 一致性Hash算法
  • 计算机基础
    • 数据结构与算法
      • 大数据处理
        • 大数据处理 - Bitmap & Bloom Filter
  • 计算机基础
    • 数据结构与算法
      • 大数据处理
        • 大数据处理 - Map & Reduce
  • 计算机基础
    • 数据结构与算法
      • 大数据处理
        • 大数据处理 - Trie树/数据库/倒排索引
  • 计算机基础
    • 数据结构与算法
      • 大数据处理
        • 大数据处理 - 分治/hash/排序
  • 计算机基础
    • 数据结构与算法
      • 大数据处理
        • 大数据处理 - 双层桶划分
  • 计算机基础
    • 数据结构与算法
      • 大数据处理
        • 大数据处理 - 外(磁盘文件)排序
  • 计算机基础
    • 数据结构与算法
      • 大数据处理
        • 大数据处理 - 布隆过滤器
  • 计算机基础
    • 数据结构与算法
      • 大数据处理
        • 大数据处理算法
  • 计算机基础
    • 数据结构与算法
      • 字符串匹配算法
        • 字符串匹配 - 文本预处理:后缀树(Suffix Tree)
  • 计算机基础
    • 数据结构与算法
      • 字符串匹配算法
        • 字符串匹配 - 模式预处理:BM 算法 (Boyer-Moore)
  • 计算机基础
    • 数据结构与算法
      • 字符串匹配算法
        • 字符串匹配 - 模式预处理:KMP 算法(Knuth-Morris-Pratt)
  • 计算机基础
    • 数据结构与算法
      • 字符串匹配算法
        • 字符串匹配 - 模式预处理:朴素算法(Naive)(暴力破解)
  • 计算机基础
    • 数据结构与算法
      • 字符串匹配算法
        • 字符串匹配
  • 计算机基础
    • 数据结构与算法
      • 常用算法
        • 分支限界算法
  • 计算机基础
    • 数据结构与算法
      • 常用算法
        • 分治算法
  • 计算机基础
    • 数据结构与算法
      • 常用算法
        • 动态规划算法
  • 计算机基础
    • 数据结构与算法
      • 常用算法
        • 回溯算法
  • 计算机基础
    • 数据结构与算法
      • 常用算法
        • 贪心算法
  • 计算机基础
    • 数据结构与算法
      • 排序算法
        • 十大排序算法
  • 计算机基础
    • 数据结构与算法
      • 排序算法
        • 图解排序算法(一)之3种简单排序(选择,冒泡,直接插入)
  • 计算机基础
    • 数据结构与算法
      • 排序算法
        • 图解排序算法(三)之堆排序
  • 计算机基础
    • 数据结构与算法
      • 排序算法
        • 图解排序算法(二)之希尔排序
  • 计算机基础
    • 数据结构与算法
      • 排序算法
        • 图解排序算法(四)之归并排序
  • 计算机基础
    • 数据结构与算法
      • 数据结构
        • 树的高度和深度
  • 计算机基础
    • 数据结构与算法
      • 数据结构
        • 红黑树深入剖析及Java实现
  • 计算机基础
    • 数据结构与算法
      • 数据结构
        • 线性结构 - Hash
  • 计算机基础
    • 数据结构与算法
      • 数据结构
        • 线性结构 - 数组、链表、栈、队列
  • 计算机基础
    • 数据结构与算法
      • 数据结构
        • 逻辑结构 - 树
  • 运维&测试
    • 测试
      • Spock
        • Groovy 简明教程
  • 运维&测试
    • 测试
      • Spock
        • Spock 官方文档
  • 运维&测试
    • 测试
      • Spock
        • Spock单元测试框架介绍以及在美团优选的实践
  • 运维&测试
    • 测试
      • TDD
        • TDD 实践 - FizzFuzzWhizz(一)
  • 运维&测试
    • 测试
      • TDD
        • TDD 实践 - FizzFuzzWhizz(三)
  • 运维&测试
    • 测试
      • TDD
        • TDD 实践 - FizzFuzzWhizz(二)
  • 运维&测试
    • 测试
      • TDD
        • 测试驱动开发(TDD)- 原理篇
  • 微服务
    • Spring Cloud
      • Spring Cloud Alibaba
        • Nacos
          • Nacos 服务注册的原理
  • 微服务
    • Spring Cloud
      • Spring Cloud Alibaba
        • Nacos
          • Nacos 配置中心原理分析
  • 微服务
    • Spring Cloud
      • Spring Cloud Alibaba
        • Seata
          • 服务调用过程
  • 微服务
    • Spring Cloud
      • Spring Cloud Alibaba
        • Seata
          • Spring Cloud Bus
  • 微服务
    • Spring Cloud
      • Spring Cloud Alibaba
        • Seata
          • Spring Cloud Consul
  • 微服务
    • Spring Cloud
      • Spring Cloud Alibaba
        • Seata
          • Spring Cloud Stream
  • 微服务
    • Spring Cloud
      • Spring Cloud Alibaba
        • Sentinel
          • Sentinel 与 Hystrix 的对比
  • 微服务
    • Spring Cloud
      • Spring Cloud Alibaba
        • Sentinel
          • Sentinel
  • 微服务
    • Spring Cloud
      • Spring Cloud Netflix
        • Hystrix
          • How Hystrix Works
  • 微服务
    • Spring Cloud
      • Spring Cloud Netflix
        • Hystrix
          • Hystrix
  • 微服务
    • Spring Cloud
      • Spring Cloud Netflix
        • Hystrix
          • Hystrix原理与实战
  • 微服务
    • Spring Cloud
      • Spring Cloud Netflix
        • Hystrix
          • Spring Cloud Hystrix基本原理
由 GitBook 提供支持
在本页
  • 1. 背景
  • 2. Spock是什么?和JUnit、jMock有什么区别?
  • 2.1 为什么使用Spock? Spock和JUnit、jMock、Mockito的区别在哪里?
  • 3. 使用Spock解决单元测试开发中的痛点
  • 3.1 单元测试代码的可读性和后期维护
  • 3.2 单元测试不仅仅是为了统计代码覆盖率,更重要的是验证业务代码的健壮性、业务逻辑的严谨性以及设计的合理性
  • 4. Mock模拟
  • 5. 强大的Where
  • 6. 异常测试
  • 7. 静态方法测试
  • 8. 动态Mock静态方法
  • 9. DAO层测试
  • 10. 覆盖率
  • 11. 参考文档

这有帮助吗?

  1. 运维&测试
  2. 测试
  3. Spock

Spock单元测试框架介绍以及在美团优选的实践

上一页Spock下一页运维&测试

最后更新于2年前

这有帮助吗?

原文链接:Spock单元测试框架介绍以及在美团优选的实践

1. 背景

​XML之父Tim Bray最近在博客里有个好玩的说法:“代码不写测试就像上了厕所不洗手……单元测试是对软件未来的一项必不可少的投资。”具体来说,单元测试有哪些收益呢?

  • 它是最容易保证代码覆盖率达到100%的测试。

  • 可以⼤幅降低上线时的紧张指数。

  • 单元测试能更快地发现问题(见下图7-1)。

  • 单元测试的性价比最高,因为错误发现的越晚,修复它的成本就越高,而且难度呈指数式增长,所以我们要尽早地进行测试(见下图7-2)。

  • 编码人员,一般也是单元测试的主要执行者,是唯一能够做到生产出无缺陷程序的人,其他任何人都无法做到这一点。

  • 有助于源码的优化,使之更加规范,快速反馈,可以放心进行重构。

这张图来自微软的统计数据:Bug在单元测试阶段被发现,平均耗时3.25小时,如果漏到系统测试阶段,要花费11.5小时。

这张图,旨在说明两个问题:85%的缺陷都在代码设计阶段产生,而发现Bug的阶段越靠后,耗费成本就越高,指数级别的增高。

尽管单元测试有如此的收益,但在我们日常的工作中,仍然存在不少项目它们的单元测试要么是不完整要么是缺失的。常见的原因总结如下:

  • 代码逻辑过于复杂;

  • 写单元测试时耗费的时间较长;

  • 任务重、工期紧,或者干脆就不写了。

基于以上问题,相较于传统的JUnit单元测试,今天为大家推荐一款名为Spock的测试框架。目前,美团优选物流技术团队绝大部分后端服务已经采用了Spock作为测试框架,在开发效率、可读性和维护性方面取得了不错的收益。

不过网上Spock资料比较简单,甚至包括官网的Demo,无法解决我们项目中复杂业务场景面临的问题,通过深入学习和实践之后,本文会将一些经验分享出来,希望能够帮助大家提高开发测试的效率。

2. Spock是什么?和JUnit、jMock有什么区别?

Spock是一款国外优秀的测试框架,基于BDD(行为驱动开发)思想实现,功能非常强大。Spock结合Groovy动态语言的特点,提供了各种标签,并采用简单、通用、结构化的描述语言,让编写测试代码更加简洁、高效。官方的介绍如下:

What is it? Spock is a testing and specification framework for Java and Groovy applications. What makes it stand out from the crowd is its beautiful and highly expressive specification language. Thanks to its JUnit runner, Spock is compatible with most IDEs, build tools, and continuous integration servers. Spock is inspired from JUnit, RSpec, jMock, Mockito, Groovy, Scala, Vulcans, and other fascinating life forms.

Spock是一个Java和Groovy应用的测试和规范框架。之所以能够在众多测试框架中脱颖而出,是因为它优美而富有表现力的规范语言。Spock的灵感来自JUnit、RSpec、jMock、Mockito、Groovy、Scala、Vulcans。

简单来讲,Spock主要特点如下:

  • 让测试代码更规范,内置多种标签来规范单元测试代码的语义,测试代码结构清晰,更具可读性,降低后期维护难度。

  • 提供多种标签,比如:given、when、then、expect、where、with、thrown……帮助我们应对复杂的测试场景。

  • 使用Groovy这种动态语言来编写测试代码,可以让我们编写的测试代码更简洁,适合敏捷开发,提高编写单元测试代码的效率。

  • 遵从BDD(行为驱动开发)模式,有助于提升代码的质量。

  • IDE兼容性好,自带Mock功能。

2.1 为什么使用Spock? Spock和JUnit、jMock、Mockito的区别在哪里?

总的来说,JUnit、jMock、Mockito都是相对独立的工具,只是针对不同的业务场景提供特定的解决方案。其中JUnit单纯用于测试,并不提供Mock功能。

我们的服务大部分是分布式微服务架构。服务与服务之间通常都是通过接口的方式进行交互。即使在同一个服务内也会分为多个模块,业务功能需要依赖下游接口的返回数据,才能继续后面的处理流程。这里的下游不限于接口,还包括中间件数据存储比如Squirrel、DB、MCC配置中心等等,所以如果想要测试自己的代码逻辑,就必须把这些依赖项Mock掉。因为如果下游接口不稳定可能会影响我们代码的测试结果,让下游接口返回指定的结果集(事先准备好的数据),这样才能验证我们的代码是否正确,是否符合逻辑结果的预期。

尽管jMock、Mockito提供了Mock功能,可以把接口等依赖屏蔽掉,但不能对静态方法Mock。虽然PowerMock、jMockit能够提供静态方法的Mock,但它们之间也需要配合(JUnit + Mockito PowerMock)使用,并且语法上比较繁琐。工具多了就会导致不同的人写出的单元测试代码“五花八门”,风格相差较大。

Spock通过提供规范性的描述,定义多种标签(given、when、then、where等),去描述代码“应该做什么”,“输入条件是什么”,“输出是否符合预期”,从语义层面规范了代码的编写。

Spock自带Mock功能,使用简单方便(也支持扩展其他Mock框架,比如PowerMock),再加上Groovy动态语言的强大语法,能写出简洁高效的测试代码,同时能方便直观地验证业务代码的行为流转,增强工程师对代码执行逻辑的可控性。

3. 使用Spock解决单元测试开发中的痛点

如果在(if/else)分支很多的复杂场景下,编写单元测试代码的成本会变得非常高,正常的业务代码可能只有几十行,但为了测试这个功能覆盖大部分的分支场景,编写的测试代码可能远不止几十行。

之前有遇到过某个功能上线很久一直都很正常,没有出现过问题,但后来有个调用请求的数据不一样,走到了代码中一个不常用的逻辑分支时,出现了Bug。当时写这段代码的同学也认为只有很小几率才能走到这个分支,尽管当时写了单元测试,但因为时间比较紧张,分支又多,就漏掉了这个分支的测试。

尽管使用JUnit的@Parametered参数化注解或者DataProvider方式可以解决多数据分支问题,但不够直观,而且如果其中某一次分支测试Case出错了,它的报错信息也不够详尽。

这就需要一种编写测试用例高效、可读性强、占用工时少、维护成本低的测试框架。首先不能让业务人员排斥编写单元测试,更不能让工程师觉得写单元测试是在浪费时间。而且使用JUnit做测试工作量不算小。据初步统计,采用JUnit的话,它的测试代码行和业务代码行能到3:1。如果采用Spock作为测试框架的话,它的比例可缩减到1:1,能够大大提高编写测试用例的效率。

下面借用《编程珠玑》中一个计算税金的例子。

public double calc(double income) {
    BigDecimal tax;
    BigDecimal salary = BigDecimal.valueOf(income);
    if (income <= 0) {
        return 0;
    }
    if (income > 0 && income <= 3000) {
        BigDecimal taxLevel = BigDecimal.valueOf(0.03);
        tax = salary.multiply(taxLevel);
    } else if (income > 3000 && income <= 12000) {
        BigDecimal taxLevel = BigDecimal.valueOf(0.1);
        BigDecimal base = BigDecimal.valueOf(210);
        tax = salary.multiply(taxLevel).subtract(base);
    } else if (income > 12000 && income <= 25000) {
        BigDecimal taxLevel = BigDecimal.valueOf(0.2);
        BigDecimal base = BigDecimal.valueOf(1410);
        tax = salary.multiply(taxLevel).subtract(base);
    } else if (income > 25000 && income <= 35000) {
        BigDecimal taxLevel = BigDecimal.valueOf(0.25);
        BigDecimal base = BigDecimal.valueOf(2660);
        tax = salary.multiply(taxLevel).subtract(base);
    } else if (income > 35000 && income <= 55000) {
        BigDecimal taxLevel = BigDecimal.valueOf(0.3);
        BigDecimal base = BigDecimal.valueOf(4410);
        tax = salary.multiply(taxLevel).subtract(base);
    } else if (income > 55000 && income <= 80000) {
        BigDecimal taxLevel = BigDecimal.valueOf(0.35);
        BigDecimal base = BigDecimal.valueOf(7160);
        tax = salary.multiply(taxLevel).subtract(base);
    } else {
        BigDecimal taxLevel = BigDecimal.valueOf(0.45);
        BigDecimal base = BigDecimal.valueOf(15160);
        tax = salary.multiply(taxLevel).subtract(base);
    }
    return tax.setScale(2, BigDecimal.ROUND_HALF_UP).doubleValue();
}

能够看到上面的代码中有大量的if-else语句,Spock提供了where标签,可以让我们通过表格的方式来测试多种分支。

@Unroll
def "个税计算,收入:#income, 个税:#result"() {
  expect: "when + then 的组合"
  CalculateTaxUtils.calc(income) == result

  where: "表格方式测试不同的分支逻辑"
  income || result
  -1     || 0
  0      || 0
  2999   || 89.97
  3000   || 90.0
  3001   || 90.1
  11999  || 989.9
  12000  || 990.0
  12001  || 990.2
  24999  || 3589.8
  25000  || 3590.0
  25001  || 3590.25
  34999  || 6089.75
  35000  || 6090.0
  35001  || 6090.3
  54999  || 12089.7
  55000  || 12090
  55001  || 12090.35
  79999  || 20839.65
  80000  || 20840.0
  80001  || 20840.45
}

上图中左边使用Spock写的单元测试代码,语法简洁,表格方式测试覆盖分支场景更加直观,开发效率高,更适合敏捷开发。

3.1 单元测试代码的可读性和后期维护

我们微服务场景很多时候需要依赖其他接口返回的结果,才能验证自己的代码逻辑。Mock工具是必不可少的。但jMock、Mockito的语法比较繁琐,再加上单元测试代码不像业务代码那么直观,又不能完全按照业务流程的思路写单元测试,这就让不少同学对单元测试代码可读性不够重视,最终导致测试代码难以阅读,维护起来更是难上加难。甚至很多同学自己写的单元测试,过几天再看也一样觉得“云里雾里”的。也有改了原来的代码逻辑导致单元测试执行失败的;或者新增了分支逻辑,单元测试没有覆盖到的;最终随着业务的快速迭代单元测试代码越来越难以维护。

Spock提供多种语义标签,如:given、when、then、expect、where、with、and等,从行为上规范了单元测试代码,每一种标签对应一种语义,让单元测试代码结构具有层次感,功能模块划分更加清晰,也便于后期的维护。

Spock自带Mock功能,使用上简单方便(Spock也支持扩展第三方Mock框架,比如PowerMock)。我们可以再看一个样例,对于如下的代码逻辑进行单元测试:

public StudentVO getStudentById(int id) {
    List<StudentDTO> students = studentDao.getStudentInfo();
    StudentDTO studentDTO = students.stream().filter(u -> u.getId() == id).findFirst().orElse(null);
    StudentVO studentVO = new StudentVO();
    if (studentDTO == null) {
        return studentVO;
    }
    studentVO.setId(studentDTO.getId());
    studentVO.setName(studentDTO.getName());
    studentVO.setSex(studentDTO.getSex());
    studentVO.setAge(studentDTO.getAge());
    // 邮编
    if ("上海".equals(studentDTO.getProvince())) {
        studentVO.setAbbreviation("沪");
        studentVO.setPostCode("200000");
    }
    if ("北京".equals(studentDTO.getProvince())) {
        studentVO.setAbbreviation("京");
        studentVO.setPostCode("100000");
    }
    return studentVO;
}

比较明显,左边的JUnit单元测试代码冗余,缺少结构层次,可读性差,随着后续的迭代,势必会导致代码的堆积,维护成本会变得越来越高。右边的单元测试代码Spock会强制要求使用given、when、then这样的语义标签(至少一个),否则编译不通过,这样就能保证代码更加规范,结构模块化,边界范围清晰,可读性强,便于扩展和维护。而且使用了自然语言描述测试步骤,让非技术人员也能看懂测试代码(given表示输入条件,when触发动作,then验证输出结果)。

Spock自带的Mock语法也非常简单:dao.getStudentInfo() >> [student1, student2]。

两个右箭头>>表示模拟getStudentInfo接口的返回结果,再加上使用的Groovy语言,可以直接使用[]中括号表示返回的是List类型。

3.2 单元测试不仅仅是为了统计代码覆盖率,更重要的是验证业务代码的健壮性、业务逻辑的严谨性以及设计的合理性

在项目初期阶段,可能为了追赶进度而没有时间写单元测试,或者这个时期写的单元测试只是为了达到覆盖率的要求(比如为了满足新增代码行或者分支覆盖率统计要求)。

很多工程师写的单元测试基本都是采用Java这种强类型语言编写,各种底层接口的Mock写起来不仅繁琐而且耗时。这时的单元测试代码可能就写得比较粗糙,有粒度过大的,也有缺少单元测试结果验证的。这样的单元测试对代码的质量帮助不大,更多是为了测试而测试。最后时间没少花,可效果却没有达到。

针对有效测试用例方面,我们测试基础组件组开发了一些检测工具(作为抓手),比如去扫描大家写的单元测试,检测单元测试的断言有效性等。另外在结果校验方面,Spock表现也是十分优异的。我们可以来看接下来的场景:void方法,没有返回结果,如何写测试这段代码的逻辑是否正确?

如何确保单元测试代码是否执行到了for循环里面的语句,循环里面的打折计算又是否正确呢?

public void calculatePrice(OrderVO order){
    BigDecimal amount = BigDecimal.ZERO;
    for (SkuVO sku : order.getSkus()) {
        Integer skuId = sku.getSkuId();
        BigDecimal skuPrice = sku.getSkuPrice();
        BigDecimal discount = BigDecimal.valueOf(discountDao.getDiscount(skuId));
        BigDecimal price = skuPrice * discount;
        amount = amount.add(price);
    }
    order.setAmount(amount.setScale(2, BigDecimal.ROUND_HALF_DOWN));
}

如果用Spock写的话,就会方便很多,如下图所示:

这里,2 * discountDao.getDiscount(_) >> 0.95 >> 0.8 在for循环中一共调用了2次,第一次返回结果0.95,第二次返回结果0.8,最后再进行验证,类似于JUnit中的Assert断言。

这样的收益还是比较明显的,不仅提高了单元测试的可控性,而且方便验证业务代码的逻辑正确性和合理性,这也是BDD思想的一种体现。

4. Mock模拟

考虑如下场景,代码如下:

@Service
public class StudentService {
    @Autowired
    private StudentDao studentDao;
    public StudentVO getStudentById(int id) {
        List<StudentDTO> students = studentDao.getStudentInfo();
        StudentDTO studentDTO = students.stream().filter(u -> u.getId() == id).findFirst().orElse(null);
        StudentVO studentVO = new StudentVO();
        if (studentDTO == null) {
            return studentVO;
        }
        studentVO.setId(studentDTO.getId());
        studentVO.setName(studentDTO.getName());
        studentVO.setSex(studentDTO.getSex());
        studentVO.setAge(studentDTO.getAge());
        // 邮编
        if ("上海".equals(studentDTO.getProvince())) {
            studentVO.setAbbreviation("沪");
            studentVO.setPostCode("200000");
        }
        if ("北京".equals(studentDTO.getProvince())) {
            studentVO.setAbbreviation("京");
            studentVO.setPostCode("100000");
        }
        return studentVO;
    }
}

其中studentDao是使用Spring注入的实例对象,我们只有拿到了返回的students,才能继续下面的逻辑(根据id筛选学生,DTO和VO转换,邮编等)。所以正常的做法是把studentDao的getStudentInfo()方法Mock掉,模拟一个指定的值,因为我们真正关心的是拿到students后自己代码的逻辑,这是需要重点验证的地方。按照上面的思路使用Spock编写的测试代码如下:

class StudentServiceSpec extends Specification {
    def studentDao = Mock(StudentDao)
    def tester = new StudentService(studentDao: studentDao)

    def "test getStudentById"() {
        given: "设置请求参数"
        def student1 = new StudentDTO(id: 1, name: "张三", province: "北京")
        def student2 = new StudentDTO(id: 2, name: "李四", province: "上海")

        and: "mock studentDao返回值"
        studentDao.getStudentInfo() >> [student1, student2]

        when: "获取学生信息"
        def response = tester.getStudentById(1)

        then: "结果验证"
        with(response) {
            id == 1
            abbreviation == "京"
            postCode == "100000"
        }
    }
}

这里主要讲解Spock的代码(从上往下)。

def studentDao = Mock(StudentDao) 这一行代码使用Spock自带的Mock方法,构造一个studentDao的Mock对象,如果要模拟studentDao方法的返回,只需studentDao.方法名() >> "模拟值"的方式,两个右箭头的方式即可。test getStudentById方法是单元测试的主要方法,可以看到分为4个模块:given、and、when、then,用来区分不同单元测试代码的作用:

  • given:输入条件(前置参数)。

  • when:执行行为(Mock接口、真实调用)。

  • then:输出条件(验证结果)。

  • and:衔接上个标签,补充的作用。

每个标签后面的双引号里可以添加描述,说明这块代码的作用(非强制),如when:"获取信息"。因为Spock使用Groovy作为单元测试开发语言,所以代码量上比使用Java写的会少很多,比如given模块里通过构造函数的方式创建请求对象。

实际上StudentDTO.java 这个类并没有3个参数的构造方法,是Groovy帮我们实现的。Groovy默认会提供一个包含所有对象属性的构造方法。而且调用方式上可以指定属性名,类似于key:value的语法,非常人性化,方便在属性多的情况下构造对象,如果使用Java写,可能就要调用很多的setXxx()方法,才能完成对象初始化的工作。

这个就是Spock的Mock用法,当调用studentDao.getStudentInfo()方法时返回一个List。List的创建也很简单,中括号[]即表示List,Groovy会根据方法的返回类型,自动匹配是数组还是List,而List里的对象就是之前given块里构造的user对象,其中 >> 就是指定返回结果,类似Mockito的when().thenReturn()语法,但更简洁一些。

如果要指定返回多个值的话,可以使用3个右箭头>>>,比如:studentDao.getStudentInfo() >>> [[student1,student2],[student3,student4],[student5,student6]]。

也可以写成这样:studentDao.getStudentInfo() >> [student1,student2] >> [student3,student4] >> [student5,student6]。

每次调用studentDao.getStudentInfo()方法返回不同的值。

public List<StudentDTO> getStudentInfo(String id){
    List<StudentDTO> students = new ArrayList<>();
    return students;
}

这个getStudentInfo(String id)方法,有个参数id,这种情况下如果使用Spock的Mock模拟调用的话,可以使用下划线_匹配参数,表示任何类型的参数,多个逗号隔开,类似于Mockito的any()方法。如果类中存在多个同名方法,可以通过 _ as参数类型 的方式区别调用,如下面的语法:

// _ 表示匹配任意类型参数
List<StudentDTO> students = studentDao.getStudentInfo(_);

// 如果有同名的方法,使用as指定参数类型区分
List<StudentDTO> students = studentDao.getStudentInfo(_ as String);
  • when模块里是真正调用要测试方法的入口tester.getStudentById()。

  • then模块作用是验证被测方法的结果是否正确,符合预期值,所以这个模块里的语句必须是boolean表达式,类似于JUnit的assert断言机制,但不必显示地写assert,这也是一种约定优于配置的思想。

  • then块中使用了Spock的with功能,可以验证返回结果response对象内部的多个属性是否符合预期值,这个相对于JUnit的assertNotNull或assertEquals的方式更简单一些。

5. 强大的Where

上面的业务代码有2个if判断,是对邮编处理逻辑:

// 邮编
if ("上海".equals(studentDTO.getProvince())) {
    studentVO.setAbbreviation("沪");
    studentVO.setPostCode("200000");
}
if ("北京".equals(studentDTO.getProvince())) {
    studentVO.setAbbreviation("京");
    studentVO.setPostCode("100000");
}

如果要完全覆盖这2个分支就需要构造不同的请求参数,多次调用被测试方法才能走到不同的分支。在前面,我们介绍了Spock的where标签可以很方便的实现这种功能,代码如下所示:

@Unroll
def "input 学生id:#id, 返回的邮编:#postCodeResult, 返回的省份简称:#abbreviationResult"() {
    given: "Mock返回的学生信息"
    studentDao.getStudentInfo() >> students

    when: "获取学生信息"
    def response = tester.getStudentById(id)

    then: "验证返回结果"
    with(response) {
        postCode == postCodeResult
        abbreviation == abbreviationResult
    }
    where: "经典之处:表格方式验证学生信息的分支场景"
    id | students                    || postCodeResult | abbreviationResult
    1  | getStudent(1, "张三", "北京") || "100000"       | "京"
    2  | getStudent(2, "李四", "上海") || "200000"       | "沪"
}

def getStudent(def id, def name, def province) {
    return [new StudentDTO(id: id, name: name, province: province)]
}

where模块第一行代码是表格的列名,多个列使用|单竖线隔开,||双竖线区分输入和输出变量,即左边是输入值,右边是输出值。格式如下:

输入参数1 | 输入参数2 || 输出结果1 | 输出结果2

而且IntelliJ IDEA支持format格式化快捷键,因为表格列的长度不一样,手动对齐比较麻烦。表格的每一行代表一个测试用例,即被测方法执行了2次,每次的输入和输出都不一样,刚好可以覆盖全部分支情况。比如id、students都是输入条件,其中students对象的构造调用了getStudent方法,每次测试业务代码传入不同的student值,postCodeResult、abbreviationResult表示对返回的response对象的属性判断是否正确。第一行数据的作用是验证返回的邮编是否是100000,第二行是验证邮编是否是200000。这个就是where+with的用法,更符合我们实际测试的场景,既能覆盖多种分支,又可以对复杂对象的属性进行验证,其中在定义的测试方法名,使用了Groovy的字面值特性:

即把请求参数值和返回结果值的字符串动态替换掉,#id、#postCodeResult、#abbreviationResult#号后面的变量是在方法内部定义的,实现占位符的功能。

@Unroll注解,可以把每一次调用作为一个单独的测试用例运行,这样运行后的单元测试结果更加直观:

而且如果其中某行测试结果不对,Spock的错误提示信息也很详细,方便进行排查(比如我们把第1条测试用例返回的邮编改成100001):

可以看出,第1条测试用例失败,错误信息是postCodeResult的预期结果和实际结果不符,业务代码逻辑返回的邮编是100000,而我们预期的邮编是100001,这样就可以排查是业务代码逻辑有问题,还是我们的断言不对。

6. 异常测试

我们再看下异常方面的测试,例如下面的代码:

 public void validateStudent(StudentVO student) throws BusinessException {
    if(student == null){
        throw new BusinessException("10001", "student is null");
    }
    if(StringUtils.isBlank(student.getName())){
        throw new BusinessException("10002", "student name is null");
    }
    if(student.getAge() == null){
        throw new BusinessException("10003", "student age is null");
    }
    if(StringUtils.isBlank(student.getTelephone())){
        throw new BusinessException("10004", "student telephone is null");
    }
    if(StringUtils.isBlank(student.getSex())){
        throw new BusinessException("10005", "student sex is null");
    }
}

BusinessException是封装的业务异常,主要包含code、message属性:

/**
 * 自定义业务异常
 */
public class BusinessException extends RuntimeException {
    private String code;
    private String message;

    setXxx...
    getXxx...
}

这个大家应该都很熟悉,针对这种抛出多个不同错误码和错误信息的异常。如果使用JUnit的方式测试,会比较麻烦。如果是单个异常还好,如果是多个的话,测试代码就不太好写。

@Test
public void testException() {
    StudentVO student = null;
    try {
        service.validateStudent(student);
    } catch (BusinessException e) {
        Assert.assertEquals(e.getCode(), "10001");
        Assert.assertEquals(e.getMessage(), "student is null");
    }

    student = new StudentVO();
    try {
        service.validateStudent(student);
    } catch (BusinessException e) {
        Assert.assertEquals(e.getCode(), "10002");
        Assert.assertEquals(e.getMessage(), "student name is null");
    }
}

当然可以使用JUnit的ExpectedException方式:

@Rule
public ExpectedException exception = ExpectedException.none();
exception.expect(BusinessException.class); // 验证异常类型
exception.expectMessage("xxxxxx"); //验证异常信息

或者使用@Test(expected = BusinessException.class) 注解,但这两种方式都有缺陷。

@Test方式不能指定断言的异常属性,比如code、message。ExpectedException的方式也只提供了expectMessage的API,对自定义的code不支持,尤其像上面的有很多分支抛出多种不同异常码的情况。接下来我们看下Spock是如何解决的。Spock内置thrown()方法,可以捕获调用业务代码抛出的预期异常并验证,再结合where表格的功能,可以很方便地覆盖多种自定义业务异常,代码如下:

@Unroll
def "validate student info: #expectedMessage"() {
    when: "校验"
    tester.validateStudent(student)

    then: "验证"
    def exception = thrown(expectedException)
    exception.code == expectedCode
    exception.message == expectedMessage

    where: "测试数据"
    student           || expectedException | expectedCode | expectedMessage
    getStudent(10001) || BusinessException | "10001"      | "student is null"
    getStudent(10002) || BusinessException | "10002"      | "student name is null"
    getStudent(10003) || BusinessException | "10003"      | "student age is null"
    getStudent(10004) || BusinessException | "10004"      | "student telephone is null"
    getStudent(10005) || BusinessException | "10005"      | "student sex is null"
}

def getStudent(code) {
    def student = new StudentVO()
    def condition1 = {
        student.name = "张三"
    }
    def condition2 = {
        student.age = 20
    }
    def condition3 = {
        student.telephone = "12345678901"
    }
    def condition4 = {
        student.sex = "男"
    }

    switch (code) {
        case 10001:
            student = null
            break
        case 10002:
            student = new StudentVO()
            break
        case 10003:
            condition1()
            break
        case 10004:
            condition1()
            condition2()
            break
        case 10005:
            condition1()
            condition2()
            condition3()
            break
    }
    return student
}

在then标签里用到了Spock的thrown()方法,这个方法可以捕获我们要测试的业务代码里抛出的异常。thrown()方法的入参xpectedException,是我们自己定义的异常变量,这个变量放在where标签里就可以实现验证多种异常情况的功能(Intellij Idea格式化快捷键,可以自动对齐表格)。expectedException类型调用validateUser方法里定义的BusinessException异常,可以验证它所有的属性,code、message是否符合预期值。

7. 静态方法测试

接下来,我们一起看下Spock如何扩展第三方PowerMock对静态方法进行测试。

Spock的单元测试代码继承自Specification基类,而Specification又是基于JUnit的注解@RunWith()实现的,代码如下:

PowerMock的PowerMockRunner也是继承自JUnit,所以使用PowerMock的@PowerMockRunnerDelegate()注解,可以指定Spock的父类Sputnik去代理运行PowerMock,这样就可以在Spock里使用PowerMock去模拟静态方法、final方法、私有方法等。其实Spock自带的GroovyMock可以对Groovy文件的静态方法Mock,但对Java代码支持不完整,只能Mock当前Java类的静态方法,官方给出的解释如下:

如下代码:

public StudentVO getStudentByIdStatic(int id) {
    List<StudentDTO> students = studentDao.getStudentInfo();

    StudentDTO studentDTO = students.stream().filter(u -> u.getId() == id).findFirst().orElse(null);
    StudentVO studentVO = new StudentVO();
    if (studentDTO == null) {
        return studentVO;
    }
    studentVO.setId(studentDTO.getId());
    studentVO.setName(studentDTO.getName());
    studentVO.setSex(studentDTO.getSex());
    studentVO.setAge(studentDTO.getAge());

    // 静态方法调用
    String abbreviation = AbbreviationProvinceUtil.convert2Abbreviation(studentDTO.getProvince());
    studentVO.setAbbreviation(abbreviation);
    studentVO.setPostCode(studentDTO.getPostCode());

    return studentVO;
}

上面使用了AbbreviationProvinceUtil.convert2Abbreviation()静态方法,对应的测试用例代码如下:

@RunWith(PowerMockRunner.class)
@PowerMockRunnerDelegate(Sputnik.class)
@PrepareForTest([AbbreviationProvinceUtil.class])
@SuppressStaticInitializationFor(["example.com.AbbreviationProvinceUtil"])
class StudentServiceStaticSpec extends Specification {
    def studentDao = Mock(StudentDao)
    def tester = new StudentService(studentDao: studentDao)

    void setup() {
        // mock静态类
        PowerMockito.mockStatic(AbbreviationProvinceUtil.class)
    }

    def "test getStudentByIdStatic"() {
        given: "创建对象"
        def student1 = new StudentDTO(id: 1, name: "张三", province: "北京")
        def student2 = new StudentDTO(id: 2, name: "李四", province: "上海")

        and: "Mock掉接口返回的学生信息"
        studentDao.getStudentInfo() >> [student1, student2]

        and: "Mock静态方法返回值"
        PowerMockito.when(AbbreviationProvinceUtil.convert2Abbreviation(Mockito.any())).thenReturn(abbreviationResult)

        when: "调用获取学生信息方法"
        def response = tester.getStudentByIdStatic(id)

        then: "验证返回结果是否符合预期值"
        with(response) {
            abbreviation == abbreviationResult
        }
        where:
        id || abbreviationResult
        1  || "京"
        2  || "沪"
    }
}

在StudentServiceStaticSpec类的头部使用@PowerMockRunnerDelegate(Sputnik.class)注解,交给Spock代理执行,这样既可以使用Spock +Groovy的各种功能,又可以使用PowerMock的对静态,final等方法的Mock。@SuppressStaticInitializationFor(["example.com.AbbreviationProvinceUtil"]),这行代码的作用是限制AbbreviationProvinceUtil类里的静态代码块初始化,因为AbbreviationProvinceUtil类在第一次调用时可能会加载一些本地资源配置,所以可以使用PowerMock禁止初始化。然后在setup()方法里对静态类进行Mock设置,PowerMockito.mockStatic(AbbreviationProvinceUtil.class)。最后在test getStudentByIdStatic测试方法里对convert2Abbreviation()方法指定返回默认值:PowerMockito.when(AbbreviationProvinceUtil.convert2Abbreviation(Mockito.any())).thenReturn(abbreviationResult)。

运行时在控制台会输出:

Notifications are not supported for behaviour ALL_TESTINSTANCES_ARE_CREATED_FIRST

这是Powermock的警告信息,不影响运行结果。

如果单元测试代码不需要对静态方法、final方法Mock,就没必要使用PowerMock,使用Spock自带的Mock()就足够了。因为PowerMock的原理是在编译期通过ASM字节码修改工具修改代码,然后使用自己的ClassLoader加载,而加载的静态方法越多,测试耗时就会越长。

8. 动态Mock静态方法

考虑场景,让静态方法每次调用返回不同的值。

以下代码:

public List<OrderVO> getOrdersBySource(){
    List<OrderVO> orderList = new ArrayList<>();
    OrderVO order = new OrderVO();
    if ("APP".equals(HttpContextUtils.getCurrentSource())) {
        if("CNY".equals(HttpContextUtils.getCurrentCurrency())){
            System.out.println("source -> APP, currency -> CNY");
        } else {
            System.out.println("source -> APP, currency -> !CNY");
        }
        order.setType(1);
    } else if ("WAP".equals(HttpContextUtils.getCurrentSource())) {
        System.out.println("source -> WAP");
        order.setType(2);
    } else if ("ONLINE".equals(HttpContextUtils.getCurrentSource())) {
        System.out.println("source -> ONLINE");
        order.setType(3);
    }
    orderList.add(order);
    return orderList;
}

这段代码的if else分支逻辑,主要是依据HttpContextUtils这个工具类的静态方法getCurrentSource()和getCurrentCurrency()的返回值来决定流程。这样的业务代码也是我们平时写单元测试时经常遇到的场景,如果能让HttpContextUtils.getCurrentSource()静态方法每次Mock出不同的值,就可以很方便地覆盖if else的全部分支逻辑。Spock的where标签可以方便地和PowerMock结合使用,让PowerMock模拟的静态方法每次返回不同的值,代码如下:

PowerMock的thenReturn方法返回的值是source和currency等2个变量,不是具体的数据,这2个变量对应where标签里的前两列source|currency。这样的写法,就可以在每次测试业务方法时,让HttpContextUtils.getCurrentSource()和HttpContextUtils.getCurrentCurrency()返回不同的来源和币种,就能轻松的覆盖if和else的分支代码。即Spock使用where表格的方式让PowerMock具有了动态Mock的功能。接下来,我们再看一下如何对于final变量进行Mock。

public List<OrderVO> convertOrders(List<OrderDTO> orders){
    List<OrderVO> orderList = new ArrayList<>();
    for (OrderDTO orderDTO : orders) {
        OrderVO orderVO = OrderMapper.INSTANCE.convert(orderDTO);
        if (1 == orderVO.getType()) {
            orderVO.setOrderDesc("App端订单");
        } else if(2 == orderVO.getType()) {
            orderVO.setOrderDesc("H5端订单");
        } else if(3 == orderVO.getType()) {
            orderVO.setOrderDesc("PC端订单");
        }
        orderList.add(orderVO);
    }
    return orderList;
}

这段代码里的for循环第一行调用了OrderMapper.INSTANCE.convert()转换方法,将orderDTO转换为orderVO,然后根据type值走不同的分支,而OrderMapper是一个接口,代码如下:

@Mapper
public interface OrderMapper {
    // 即使不用static final修饰,接口里的变量默认也是静态、final的
    static final OrderMapper INSTANCE = Mappers.getMapper(OrderMapper.class);

    @Mappings({})
    OrderVO convert(OrderDTO requestDTO);
}

INSTANCE是接口OrderMapper里定义的变量,接口里的变量默认都是static final的,所以我们要先把这个INSTANCE静态final变量Mock掉,这样才能调用它的方法convert()返回我们想要的值。OrderMapper这个接口是mapstruct工具的用法,mapstruct是做对象属性映射的一个工具,它会自动生成OrderMapper接口的实现类,生成对应的set、get方法,把orderDTO的属性值赋给orderVO属性,通常情况下会比使用反射的方式好不少。看下Spock如何写这个单元测试:

@Unroll
def "test convertOrders"() {
  given: "Mock掉OrderMapper的静态final变量INSTANCE,并结合Spock设置动态返回值"
  def orderMapper = Mock(OrderMapper.class)
  Whitebox.setInternalState(OrderMapper.class, "INSTANCE", orderMapper)
  orderMapper.convert(_) >> order

  when: 
  def orders = service.convertOrders([new OrderDTO()])

  then: "验证结果"
  with(orders) {
    it[0].orderDesc == desc
  }

  where: "测试数据"
  order                || desc
  new OrderVO(type: 1) || "App端订单"
  new OrderVO(type: 2) || "H5端订单"
  new OrderVO(type: 3) || "PC端订单"
}
  • 首先使用Spock自带的Mock()方法,将OrderMapper类Mock为一个模拟对象orderMapper,def orderMapper = Mock(OrderMapper.class)。

  • 然后使用PowerMock的Whitebox.setInternalState(),对OrderMapper接口的static final常量INSTANCE赋值(Spock不支持静态常量的Mock),赋的值正是使用SpockMock的对象orderMapper。

  • 使用Spock的Mock模拟convert()方法调用,orderMapper.convert(_) >> order,再结合where表格,实现动态Mock接口的功能。

主要是这3行代码:

def orderMapper = Mock(OrderMapper.class) // 先使用Spock的Mock
Whitebox.setInternalState(OrderMapper.class, "INSTANCE", orderMapper) // 通过PowerMock把Mock对象orderMapper赋值给静态常量INSTANCE
orderMapper.convert(_) >> order // 结合where模拟不同的返回值

这样就可以使用Spock结合PowerMock测试静态常量,达到覆盖if else不同分支逻辑的功能。

9. DAO层测试

DAO层的测试有些不太一样,不能再使用Mock,否则无法验证SQL是否正确。对于DAO测试有一般最简的方式是直接使用@SpringBootTest注解启动测试环境,通过Spring创建Mybatis、Mapper实例,但这种方式并不属于单元测试,而是集成测试范畴了,因为当启用@SpringBootTest时,会把整个应用的上下文加载进来。不仅耗时时间长,而且一旦依赖环境上有任何问题,可能会影响启动,进而影响DAO层的测试。最后,需要到数据库尽可能隔离,因为如果大家都使用同一个Test环境的数据的话,一旦测试用例编写有问题,就可能会污染Test环境的数据。

针对以上场景,可采用以下方案:

  1. 通过MyBatis的SqlSession启动mapper实例(避免通过Spring启动加载上下文信息)。

  2. 通过内存数据库(如H2)隔离大家的数据库连接(完全隔离不会存在互相干扰的现象)。

  3. 通过DBUnit工具,用作对于数据库层的操作访问工具。

  4. 通过扩展Spock的注解,提供对于数据库Schema创建和数据Data加载的方式。如csv、xml或直接Closure编写等。

在pom文件增加相应的依赖。

<dependency>
     <groupId>com.h2database</groupId>
     <artifactId>h2</artifactId>
     <version>1.4.200</version>
     <scope>test</scope>
 </dependency>
 <dependency>
     <groupId>org.dbunit</groupId>
     <artifactId>dbunit</artifactId>
     <version>2.5.1</version>
     <scope>test</scope>
 </dependency>

增加Groovy的maven插件、资源文件拷贝以及测试覆盖率统计插件。

<!-- 测试插件 -->
<plugin>
  <groupId>org.codehaus.gmavenplus</groupId>
  <artifactId>gmavenplus-plugin</artifactId>
  <version>1.8.1</version>
  <executions>
    <execution>
      <goals>
        <goal>addSources</goal>
        <goal>addTestSources</goal>
        <goal>generateStubs</goal>
        <goal>compile</goal>
        <goal>generateTestStubs</goal>
        <goal>compileTests</goal>
        <goal>removeStubs</goal>
        <goal>removeTestStubs</goal>
      </goals>
    </execution>
  </executions>
</plugin>
<plugin>
  <groupId>org.apache.maven.plugins</groupId>
  <artifactId>maven-surefire-plugin</artifactId>
  <version>3.0.0-M3</version>
  <configuration>
    <useFile>false</useFile>
    <includes>
      <include>**/*Spec.java</include>
    </includes>
    <parallel>methods</parallel>
    <threadCount>10</threadCount>
    <testFailureIgnore>true</testFailureIgnore>
  </configuration>
</plugin>
<plugin>
  <groupId>org.apache.maven.plugins</groupId>
  <artifactId>maven-resources-plugin</artifactId>
  <version>2.6</version>
  <executions>
    <execution>
      <id>copy-resources</id>
      <phase>compile</phase>
      <goals>
        <goal>copy-resources</goal>
      </goals>
      <configuration>
        <outputDirectory>${basedir}/target/resources</outputDirectory>
        <resources>
          <resource>
            <directory>${basedir}/src/main/resources</directory>
            <filtering>true</filtering>
          </resource>
        </resources>
      </configuration>
    </execution>
  </executions>
</plugin>
<plugin>
  <groupId>org.jacoco</groupId>
  <artifactId>jacoco-maven-plugin</artifactId>
  <version>0.8.2</version>
  <executions>
    <execution>
      <id>prepare-agent</id>
      <goals>
        <goal>prepare-agent</goal>
      </goals>
    </execution>
    <execution>
      <id>report</id>
      <phase>prepare-package</phase>
      <goals>
        <goal>report</goal>
      </goals>
    </execution>
    <execution>
      <id>post-unit-test</id>
      <phase>test</phase>
      <goals>
        <goal>report</goal>
      </goals>
      <configuration>
        <dataFile>target/jacoco.exec</dataFile>
        <outputDirectory>target/jacoco-ut</outputDirectory>
      </configuration>
    </execution>
  </executions>
</plugin>

加入对于Spock扩展的自动处理框架(用于数据Schema和Data初始化操作)。

这里介绍一下主要内容,注解@MyDbUnit:

@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
@ExtensionAnnotation(MyDbUnitExtension.class)
@interface MyDbUnit {
    /**
     * <pre>
     * content = {
     *    your_table_name(id: 1, name: 'xxx', age: 21)
     *    your_table_name(id: 2, name: 'xxx', age: 22)
     * })
     </pre>
     * @return
     */
    Class<? extends Closure> content() default Closure.class;
    /**
     * xml存放路径(相对于测试类)
     * @return
     */
    String xmlLocation() default "";
    /**
     * csv存放路径(相对于测试类)
     * @return
     */
    String csvLocation() default "";
}

考虑以下代码的测试:

@Repository("personInfoMapper")
public interface PersonInfoMapper {
    @Delete("delete from person_info where id=#{id}")
    int deleteById(Long id);

    @Select("select count(*) from person_info")
    int count();

    @Select("select * from user_info")
    List<PersonInfoDO> selectAll();
}

Demo1 (使用@MyDbUnit,content指定导入数据内容,格式Closure)。

class Demo1Spec extends MyBaseSpec {

    /**
     * 直接获取待测试的mapper
     */
    def personInfoMapper = MapperUtil.getMapper(PersonInfoMapper.class)

    /**
     * 测试数据准备,通常为sql表结构创建用的ddl,支持多个文件以逗号分隔。
     */
    def setup() {
        executeSqlScriptFile("com/xxx/xxx/xxx/......../schema.sql")
    }
    /**
     * 数据表清除,通常待drop的数据表
     */
    def cleanup() {
        dropTables("person_info")
    }

    /**
     * 直接构造数据库中的数据表,此方法适用于数据量较小的mapper sql测试
     */
    @MyDbUnit(
            content = {
                person_info(id: 1, name: "abc", age: 21)
                person_info(id: 2, name: "bcd", age: 22)
                person_info(id: 3, name: "cde", age: 23)
            }
    )
    def "demo1_01"() {
        when:
        int beforeCount = personInfoMapper.count()
        // groovy sql用于快速执行sql,不仅能验证数据结果,也可向数据中添加数据。
        def result = new Sql(dataSource).firstRow("select * from `person_info`") 
        int deleteCount = personInfoMapper.deleteById(1L)
        int afterCount = personInfoMapper.count()

        then:
        beforeCount == 3
        result.name == "abc"
        deleteCount == 1
        afterCount == 2
    }

    /**
     * 直接构造数据库中的数据表,此方法适用于数据量较小的mapper sql测试
     */
    @MyDbUnit(content = {
        person_info(id: 1, name: 'a', age: 21)
    })
    def "demo1_02"() {
        when:
        int beforeCount = personInfoMapper.count()
        def result = new Sql(dataSource).firstRow("select * from `person_info`")
        int deleteCount = personInfoMapper.deleteById(1L)
        int afterCount = personInfoMapper.count()

        then:
        beforeCount == 1
        result.name == "a"
        deleteCount == 1
        afterCount == 0
    }
}

在setup()阶段,把数据库表中的Schema创建好,然后通过下面的@MyDbUnit注解的content属性,把数据导入到数据库中。person_info是表名,id、name、age是数据。

通过MapperUtil.getMapper()方法获取mapper实例。

当测试数据量较大时,可以编写相应的数据文件,通过@MyDbUnit的xmlLocation或csvLocation加载文件(分别支持csv和xml格式)。

如通过csv加载文件,csvLocation指向csv文件所在文件夹。

@MyDbUnit(csvLocation = "com/xxx/........./data01")
def "demo2_01"() {
    when:
    int beforeCount = personInfoMapper.count()
    def result = new Sql(dataSource).firstRow("select * from `person_info`")
    int deleteCount = personInfoMapper.deleteById(1L)
    int afterCount = personInfoMapper.count()

    then:
    beforeCount == 3
    result.name == "abc"
    deleteCount == 1
    afterCount == 2
}

通过xml加载文件,xmlLocation指向xml文件所在路径。

@MyDbUnit(xmlLocation = "com/xxxx/........./demo3_02.xml")
def "demo3_02"() {
    when:
    int beforeCount = personInfoMapper.count()
    def result = new Sql(dataSource).firstRow("select * from `person_info`")
    int deleteCount = personInfoMapper.deleteById(1L)
    int afterCount = personInfoMapper.count()

    then:
    beforeCount == 1
    result.name == "a"
    deleteCount == 1
    afterCount == 0
}

还可以不通过@MyDbUnit而使用API直接加载测试数据文件。

class Demo4Spec extends MyBaseSpec {
    def personInfoMapper = MapperUtil.getMapper(PersonInfoMapper.class)

    /**
     * 数据表清除,通常待drop的数据表
     */
    def cleanup() {
        dropTables("person_info")
    }
    def "demo4_01"() {
        given:
        executeSqlScriptFile("com/xxxx/.........../schema.sql")
        IDataSet dataSet = MyDbUnitUtil.loadCsv("com/xxxx/.........../data01");
        DatabaseOperation.CLEAN_INSERT.execute(MyIDatabaseConnection.getInstance().getConnection(), dataSet);

        when:
        int beforeCount = personInfoMapper.count()
        def result = new Sql(dataSource).firstRow("select * from `person_info`")
        int deleteCount = personInfoMapper.deleteById(1L)
        int afterCount = personInfoMapper.count()

        then:
        beforeCount == 3
        result.name == "abc"
        deleteCount == 1
        afterCount == 2
    }

    def "demo4_02"() {
        given:
        executeSqlScriptFile("com/xxxx/.........../schema.sq")
        IDataSet dataSet = MyDbUnitUtil.loadXml("com/xxxx/.........../demo3_02.xml");
        DatabaseOperation.CLEAN_INSERT.execute(MyIDatabaseConnection.getInstance().getConnection(), dataSet);

        when:
        int beforeCount = personInfoMapper.count()
        def result = new Sql(dataSource).firstRow("select * from `person_info`")
        int deleteCount = personInfoMapper.deleteById(1L)
        int afterCount = personInfoMapper.count()

        then:
        beforeCount == 1
        result.name == "a"
        deleteCount == 1
        afterCount == 0
    }
}

10. 覆盖率

Jacoco是统计单元测试覆盖率的一种工具,当然Spock也自带了覆盖率统计的功能,这里使用第三方Jacoco的原因主要是国内公司使用的比较多一些,包括美团很多技术团队现在使用的也是Jacoco,所以为了兼容就以Jacoco来查看单元测试覆盖率。这里说下如何通过Jacoco确认分支是否完全覆盖到。

在pom文件里引用Jacoco的插件:jacoco-maven-plugin,然后执行mvn package 命令,成功后会在target目录下生成单元测试覆盖率的报告,点开报告找到对应的被测试类查看覆盖情况。

绿色背景表示完全覆盖,黄色是部分覆盖,红色没有覆盖到。比如第34行黄色背景的else if() 判断,提示有二分之一的分支缺失,虽然它下面的代码也被覆盖了(显示为绿色),这种情况跟具体使用哪种单元测试框架没关系,因为这只是分支覆盖率统计的规则,只不过使用Spock的话,解决起来会更简单,只需在where下增加一行针对的测试数据即可。

11. 参考文档

  • Spock Framework Reference Documentation

  • 老K的Java博客

2021-08-25-9MsMqo
2021-08-25-JLTq6v
2021-08-25-7Pca31
2021-08-25-erkCpx
2021-08-25-FD2vXT
2021-08-25-pIp4mW
2021-08-25-2mgYMT
2021-08-25-vX5Fqp
2021-08-25-HjexW5
2021-08-25-TAYmCn
2021-08-25-d8Ulm1
2021-08-25-MxZqnL
2021-08-25-dAfGuO
2021-08-25-V9YEwh
2021-08-25-JarkGe
2021-08-25-kdXLs4
2021-08-25-HLC4aX
2021-08-25-j79zvk
2021-08-25-l7L0J1
2021-08-25-fAyJmH
2021-08-25-dscmEa
2021-08-25-qjqv9j