- 内容提要
- 作者简介
- 译者简介
- 前言
- HTTP
- Servlet 和 JSP
- 下载 Spring 或使用 STS 与 Maven/Gradle
- 手动下载 Spring
- 使用 STS 和 Maven/Gradle
- 下载 Spring 源码
- 本书内容简介
- 下载示例应用
- 第 1 章Spring 框架
- 第 2 章模型 2 和 MVC 模式
- 第 3 章Spring MVC 介绍
- 第 4 章基于注解的控制器
- 第 5 章数据绑定和表单标签库
- 第 6 章转换器和格式化
- 第 7 章验证器
- 第 8 章表达式语言
- 第 9 章JSTL
- 第 10 章国际化
- 第 11 章上传文件
- 第 12 章下载文件
- 第 13 章应用测试
- 附录 A Tomcat
- 附录 B Spring Tool Suite 和 Maven
- 附录 C Servlet
- 附录 D JavaServer Pages
- 附录 E 部署描述符
13.4 应用测试挡板(Test Doubles)
被测系统(SUT)很少孤立存在。通常为了测试一个类,你需要依赖。在测试中,你的 SUT 所需要的依赖称为协作者。
协作者经常被称为测试挡板 ① 的其他对象取代。测试挡板是 Gerard Meszaros 在他的书《xUnit Test Patterns: Refaltoning your code》中创造的一个术语。他对该术语的解释可以在这个网页中找到:http://xunitpatterns.com/Test%20Double.html
使用测试挡板有几个原因。
在编写测试类时,真正的依赖还没有准备好。
一些依赖项,例如 HttpServletRequest 和 HttpServletResponse 对象,是从 servlet 容器获取的,而自己创建这些对象将会非常耗时。
一些依赖关系启动和初始化速度较慢。例如,DAO 对象访问数据库导致单元测试执行很慢。
测试挡板在单元测试中广泛使用,也用于集成测试。当前有许多用于创建测试挡板的框架。Spring 也有自己的类来创建测试挡板。
模拟框架可用于创建测试挡板和验证代码行为。这里有一些流行的框架。
Mockito。
EasyMock。
jMock。
除了上面的库,Spring 还附带了创建模拟对象的类。在下一节中,我们将使用 Spring 类来测试 Spring MVC 控制器。在本节中,我们将学习使用 Mockito。
Mockito 无法直接下载,所以你必须使用 Maven。但是,本章的示例项目包括 Mockito 发布包(一个 mockito.jar 文件)及其依赖(一个 objenesis.jar 文件)。
要使用 Maven 下载 Mockito,请将以下依赖关系添加到 pom.xml 文件中。
<dependency>
<groupId>org.mockito</groupId>
<artifactId>mockito-core</artifactId>
<version>2.0.43-beta</version>
<type>pom</type>
</dependency>
在开始写测试挡板之前,你应该先学习理论知识。如下是测试挡板的 5 种类型。
dummy;
stub;
spy;
fake;
mock。
这些类型中的每一种将在下面的小节中解释。
13.4.1 dummy
dummy 是最基本的测试挡板类型。一个 dummy 是一个协作者的实现,它不做任何事情,并且不改变 SUT 的行为。它通常用于使 SUT 可以实例化。dummy 只在开发的早期阶段使用。
例如,考虑清单 13.6 中的 ProductServiceImpl 类。这个类依赖于传递给构造函数的 ProductDAO。
清单 13.6 ProductServiceImpl 类
package com.example.service;
import com.example.dao.ProductDAO;
public class ProductServiceImpl implements ProductService {
private ProductDAO productDAO;
public ProductServiceImpl(ProductDAO productDAOArg) {
if (productDAOArg == null) {
throw new NullPointerException("ProductDAO cannot be null.");
}
this.productDAO = productDAOArg;
}
@Override
public BigDecimal calculateDiscount() {
return productDAO.calculateDiscount();
}
@Override
public boolean isOnSale(int productId) {
return productDAO.isOnSale(productId);
}
}
ProductServiceImpl 类需要一个非空的 ProductDAO 对象来实例化。同时,要测试的方法不使用 ProductDAO。因此,可以创建一个如清单 13.7 所示的 dummy 对象,只需让 ProductServiceImpl 可实例化。
清单 13.7 ProductDAODummy 类
package com.example.dummy;
import java.math.BigDecimal;
import com.example.dao.ProductDAO;
public class ProductDAODummy implements ProductDAO {
public BigDecimal calculateDiscount() {
return null;
}
public boolean isOnSale(int productId) {
return false;
};
}
在 dummy 类中的方法实现什么也不做,它的返回值也不重要,因为这些方法从未使用过。
清单 13.8 显示了一个可以运行的测试类
清单 13.8 ProductDAOTest 类
package com.example.dummy;
import static org.junit.Assert.assertNotNull;
import org.junit.Test;
import com.example.dao.ProductDAO;
import com.example.service.ProductService;
import com.example.service.ProductServiceImpl;
public class ProductServiceImplTest {
@Test
public void testCalculateDiscount() {
ProductDAO productDAO = new ProductDAODummy();
ProductService productService =
new ProductServiceImpl(productDAO);
assertNotNull(productService);
}
}
13.4.2 stub
像 dummy 一样,stub 也是依赖接口的实现。和 dummy 不同的是,stub 中的方法返回硬编码值,并且这些方法被实际调用。
清单 13.9 显示了一个 stub,可用于测试清单 13.6 中的 ProductServiceImpl 类。
清单 13.9 ProductDAOStub 类
package com.example.stub;
import java.math.BigDecimal;
import com.example.dao.ProductDAO;
public class ProductDAOStub implements ProductDAO {
public BigDecimal calculateDiscount() {
return new BigDecimal(14);
}
public boolean isOnSale(int productId) {
return false;
};
}
13.4.3 spy
spy 是一个略微智能一些的 stub,因为 spy 可以保留状态。考虑下面的汽车租赁应用程序,其中包含一个 GarageService 接口和一个 GarageServiceImpl 类,分别在清单 13.10 和清单 13.11 中。
清单 13.10 GarageService 接口
package com.example.service;
import com.example.Car;
public interface GarageService {
Car rent();
}
清单 13.11 GarageServiceImpl 类
package com.example.service;
import com.example.Car;
import com.example.dao.GarageDAO;
public class GarageServiceImpl implements GarageService {
private GarageDAO garageDAO;
public GarageServiceImpl(GarageDAO garageDAOArg) {
this.garageDAO = garageDAOArg;
}
public Car rent() {
return garageDAO.rent();
}
}
GarageService 接口只有一个方法:rent。GarageServiceImpl 类是 GarageService 的一个实现,并且依赖一个 GarageDAO。GarageServiceImpl 中的 rent 方法调用 GarageDAO 中的 rent 方法。GarageDAO 的实现应该返回一个 car,如果还有一辆汽车在车库;或者返回 null,如果没有更多的汽车。
由于 GarageDAO 的真正实现还没有完成,清单 13.12 中的 GarageDAOSpy 类被用作测试挡板。它是一个 spy,因为它的方法返回一个硬编码值,并且它通过一个 carCount 变量来保存车库里的车数。
清单 13.12 GarageDAOSpy 类
package com.example.spy;
import com.example.Car;
import com.example.dao.GarageDAO;
public class GarageDAOSpy implements GarageDAO {
private int carCount = 3;
@Override
public Car rent() {
if (carCount == 0) {
return null;
} else {
carCount--;
return new Car();
}
}
}
清单 13.13 显示了使用 GarageDAOSpy 测试 GarageServiceImpl 类的一个测试类。
清单 13.13 GarageServiceImplTest 类
package com.example.spy;
import com.example.Car;
import com.example.dao.GarageDAO;
import com.example.service.GarageService;
import com.example.service.GarageServiceImpl;
import org.junit.Test;
import static org.junit.Assert.*;
public class GarageServiceImplTest {
@Test
public void testRentCar() {
GarageDAO garageDAO = new GarageDAOSpy();
GarageService garageService = new GarageServiceImpl(garageDAO);
Car car1 = garageService.rent();
Car car2 = garageService.rent();
Car car3 = garageService.rent();
Car car4 = garageService.rent();
assertNotNull(car1);
assertNotNull(car2);
assertNotNull(car3);
assertNull(car4);
}
}
由于在车库只有 3 辆车,spy 只能返回 3 辆车,当第四次调用其 rent 方法时,返回 null。
13.4.4 fake
fake 的行为就像一个真正的合作者,但不适合生产,因为它走“捷径”。内存存储是一个 fake 的完美示例,因为它的行为像一个 DAO,但不会将其状态保存到硬盘驱动器。
例如,分别考虑清单 13.14 和清单 13.15 中的 Member 和 MemberServiceImpl 类。Member 类包含成员的标识符和名称。MemberServiceImpl 类可以将成员添加到商店并检索所有存储的成员。
清单 13.14 Member 类
package com.example.model;
public class Member {
private int id;
private String name;
public Member(int idArg, String nameArg) {
this.id = idArg;
this.name = nameArg;
}
public int getId() {
return id;
}
public void setId(int idArg) {
this.id = idArg;
}
public String getName() {
return name;
}
public void setName(String nameArg) {
this.name = nameArg;
}
}
清单 13.15 MemberServiceImpl 类
package com.example.service;
import java.util.List;
import com.example.dao.MemberDAO;
import com.example.model.Member;
public class MemberServiceImpl implements MemberService {
private MemberDAO memberDAO;
public void setMemberDAO(MemberDAO memberDAOArg) {
this.memberDAO = memberDAOArg;
}
@Override
public void add(Member member) {
memberDAO.add(member);
}
@Override
public List<Member> getMembers() {
return memberDAO.getMembers();
}
}
MemberServiceImpl 依赖于 MemberDAO。但是,由于没有可用的 MemberDAO 实现,你可以创建一个 MemberDAO 的 fake 实现,以便可以立即测试 MemberServiceImpl。清单 13.16 显示了这样一个 fake。它将成员存储在 ArrayList 中,而不是持久化存储。因此,不能在生产中使用它,但是对于单元测试是足够的。
清单 13.16 MemberDAOFake 类
package com.example.fake;
import java.util.ArrayList;
import java.util.List;
import com.example.dao.MemberDAO;
import com.example.model.Member;
public class MemberDAOFake implements MemberDAO {
private List<Member> members = new ArrayList<>();
@Override
public void add(Member member) {
members.add(member);
}
@Override
public List<Member> getMembers() {
return members;
}
}
清单 13.17 显示了一个测试类,它使用 MemberDAOFake 作为 MemberDAO 的测试挡板类来测试 MemberServiceImpl。
清单 13.17 MemberServiceImplTest 类
package com.example.service;
import org.junit.Assert;
import org.junit.Test;
import com.example.dao.MemberDAO;
import com.example.fake.MemberDAOFake;
import com.example.model.Member;
public class MemberServiceImplTest {
@Test
public void testAddMember() {
MemberDAO memberDAO = new MemberDAOFake();
memberDAO.add(new Member(1, "John Diet"));
memberDAO.add(new Member(2, "Jane Biteman"));
Assert.assertEquals(2, memberDAO.getMembers().size());
}
}
13.4.5 mock
mock 在理念上不同于其他测试挡板。使用 dummy、stub、spy 和 fake 来进行状态测试,即验证方法的输出。而使用 mock 来执行行为(交互)测试,以确保某个方法真正被调用,或者验证一个方法在执行另一个方法期间被调用了一定的次数。
例如,考虑清单 13.18 中的 MathUtil 类。
清单 13.18 MathUtil 类
package com.example;
public class MathUtil {
private MathHelper mathHelper;
public MathUtil(MathHelper mathHelper) {
this.mathHelper = mathHelper;
}
public MathUtil() {
}
public int multiply(int a, int b) {
int result = 0;
for (int i = 1; i <= a; i++) {
result = mathHelper.add(result, b);
}
return result;
}
}
MathUtil 有一个方法 multiply。它非常直接,使用多个 add 方法。换句话说,3 × 8 计算为 8 + 8 + 8。MathUtil 类甚至不知道如何执行 add。为此,它依赖于 MathHelper 对象,其类在列表 13.19 中结出。
清单 13.19 MathHelper 类
package com.example;
public class MathHelper {
public int add(int a, int b) {
return a + b;
}
}
测试所关心的不是 multiply 方法的结果,而是找出方法是否如预期一样运行。因此,在计算 3 × 8 时,它应该调用 MathHelper.add() 3 次。清单 13.20 显示了一个使用 MathHelper 模拟的测试类。Mockito 是一个流行的模拟框架,用于创建模拟对象。我将在本章稍后介绍关于 Mockito 的更多知识。在这一节中,我只是想告诉你这个概念。
清单 13.20 MathUtilTest 类
package com.example;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
import org.junit.Test;
public class MathUtilTest {
@Test
public void testMultiply() {
MathHelper mathHelper = mock(MathHelper.class);
for (int i = 0; i < 10; i++) {
when(mathHelper.add(i * 8, 8)).thenReturn(i * 8 + 8);
}
MathUtil mathUtil = new MathUtil(mathHelper);
mathUtil.multiply(3, 8);
verify(mathHelper, times(1)).add(0, 8);
verify(mathHelper, times(1)).add(8, 8);
verify(mathHelper, times(1)).add(16, 8);
}
}
使用 Mockito 创建 mock 对象非常简单,只需调用 org.mockito.Mockito 类的静态 mock 方法即可。下面展示如何创建 MathHelper mock 对象。
MathHelper mathHelper = mock(MathHelper.class);
接下来,你需要使用 when 方法准备 mock 对象。基本上,你告诉它,给定使用这组参数的方法调用,mock 对象必须返回这个值。例如,这条语句是说如果调用 mathHelper.add(10,20),返回值必须是 10 + 20:
when(mathHelper.add(10,20) ).thenReturn(10 + 20);
对于此测试,你准备具有十组参数的 mock 对象(但不是所有的参数组都会被使用)。
for (int i = 0; i < 10; i++) {
when(mathHelper.add(i * 8, 8)).thenReturn(i * 8 + 8);
}
然后创建要测试的对象并调用其参数。
MathUtil mathUtil = new MathUtil(mathHelper);
mathUtil.multiply(3,8);
接下来的 3 条语句是行为测试。为此,调用 verify 方法:
verify(mathHelper,times(1)).add(0,8);
verify(mathHelper,times(1)).add(8,8);
verify(mathHelper,times(1)).add(16,8);
第一条语句验证 mathHelper.add(0,8)被调用了一次。第二条语句验证 mathHelper.add(8,8)被调用一次,第三条语句验证 mathHelper.add(16,8)也被调用一次。
绑定邮箱获取回复消息
由于您还没有绑定你的真实邮箱,如果其他用户或者作者回复了您的评论,将不能在第一时间通知您!
发布评论