本章主要介绍Spring Boot单元测试、Mockito/PowerMockito测试框架、H2内存型数据库、REST API测试以及性能测试等内容。
软件测试的目的是保证程序员编写的程序达到预期的结果,保证发布的产品是产品经理(产品设计人员)的真实意愿表现。这些都需要软件测试来监督实现,避免将有缺陷的软件发布到生产环境。
软件测试的种类很多,粗略地可划分为单元测试、集成测试、端到端测试。从其他的角度来说,又有回归测试、自动化测试、性能测试等。当我们的项目进行服务化改造之后,尤其是进行了微服务设计之后,测试工作会变得更加困难。很多项目都是以独立服务的形式发布的,这些服务的发布如何保证已经进行充分测试?测试的入口应该在哪里?是直接进行集成测试,还是做端到端的用户体验测试?好像都不太合适。按照分层测试的思想,于是就有了服务测试的话题。微服务的测试理论和其他的测试应该是大体类似的,其中比较特殊的是,如何提供方便快捷的服务测试入口。
目前常见的微服务设计都采用分布式服务框架,这些框架从通信协议上可分为两种:
基于公共标准的HTTP协议的
第一种以HTTP协议的微服务接口比如使用Spring Boot开发的服务,这样的服务测试工具有很多,比如Postman、Swagger是常用的工具。如果想为测试人员做点事情的话,可以根据服务注册中心做一个所有服务的列表。
基于私有的RPC调用协议
第二种是以私有协议暴露的服务测试,相对比较麻烦。为了打通服务接口和测试人员之间的屏障,以便让测试人员方便地测试到RPC协议的服务接口,为每个服务接口写一个客户端,将其转换为HTTP协议暴露,这是一种解决办法。但是,这样无形中增加了很多工作量,而且测试服务的质量还依赖于客户端编写的质量,明显是费力不讨好的工作。
那么,如何构建一个项目,它能提供所有服务的客户端,这样新开发一个服务只需要做极少的工作就能生成一个服务的测试客户端,从而快速地将接口提交测试,这是我们下面要讨论的问题。
微服务设计的项目一般都是基于分布式服务的注册和发现机制的,所有的服务都是在一个注册中心集中存储的,而且一般的分布式服务框架都支持丰富的服务调用方式,如基于Spring XML配置的和Spring注解以及API等调用方法,为编写公共的服务测试工具提供了便利的条件。
其所设计的服务测试工具在整个分布式服务架构中所扮演的角色如图所示。
微服务测试的流程可描述为以下5个步骤:
微服务测试的宗旨就是尽可能地简化服务测试过程,其中还有一些服务测试基础功能之外的拓展功能:
项目在投入生产之前,需要进行大量的单元测试,Spring Boot作为分布式微服务架构的脚手架,非常有必要来了解下Spring Boot如何进行单元测试。具体步骤如下:
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>springstarter--boot--testartifactId>
<scope>testscope>
dependency>
spring-boot-starter-test插件依赖了spring-boot-test、junit、assertj、mockito、hamcrest等测试框架和类库。
// 用户接口
public interface UserService{
AyUser findUser(String id);
}
// UserServiceImple实现类
@Component
public class UserServiceImpl implements UserService{
@Override
public AyUser findUser(String id) {
AyUser ayUser = new AyUser();
ayUser.setId(1);
ayUser.setName("ay");
return ayUser;
}
}
//用户实体
public class AyUser{
private Integer id;
private String name;
}
@RunWith(SpringRunner.class)
@SpringBootTest
public class DemoApplicationTests {
@Resource
private UserService userService;
@Test
public void contextLoads(){}
@Test
public void testFindUser(){
AyUser ayUser = userService.findUser("1");
Assert.assertNotNull("user is null", ayUser);
}
}
当右键执行DemoApplicationTests.java中的contextLoads方法时,可以看到控制台打印的信息和执行入口类中的SpringApplication.run()方法打印的信息是一致的。由此便知,@SpringBootTest是引入了入口类的配置。
在DemoApplicationTests.java类中添加测试用例testFindUser,并在方法上添加@Test注解,运行测试用例,通过使用JUnit框架提供的Assert.assertXXX()断言方法来验证期望值与实际值是否一致。如果不一致,将打印错误信息“user is null”,这就是单元测试的基本做法。
JUnit框架提供的Assert断言一方面需要提供错误信息,另一方面期望值与实际值到底谁在前谁在后,很容易犯错。好在Spring Boot已经考虑到这些因素,它依赖于AssertJ类库,弥补了JUnit框架在断言方面的不足之处。我们可以轻松地将JUnit断言修改为AssertJ断言,具体代码如下:
@Test
public void testFindUser(){
boolean success false;
int num 10;
AyUser ayUser = userService.findUser("1");
//JUnit断言
Assert.assertNotNull("user is null", ayUser);
//AssertJ断言
Assertions.assertThat(ayUser.isNotNull());
//JUnit断言
Assert.assertTrue("result is not true", success);
//AssertJ断言
Assertions.assertThat(success).isTrue();
//JUnit断言
Assert.assertEquals("num is not equal 10", 10, num);
//AssertJ断言
Assertions.assertThat(num).isEqualTo(10);
}
Mockito是用于生成模拟对象或者直接说就是“假对象”的模拟工具。其特点是对于某些不容易构造(如HttpServletRequest)或者不容易获取的复杂对象(如JDBC中的ResultSet对象),可用一个虚拟的对象(Mock对象)来创建以便完成测试。Mockito最大的优点是可帮你把单元测试的耦合分解开,如果你的代码对另一个类或者接口有依赖,它能够帮助你模拟这些依赖,并帮助你验证所调用的依赖的行为。我们先来看一个传统的测试用例调用流程图,如图所示。
当想要测试用户服务类UserService的某些接口时,需要依赖UserDao对象来完成相关测试,而UserDao对象还需要连接数据库。某些情况下我们无法连接数据库,比如无网络的情况下,此时,测试用例就无法正常执行。清楚了传统JUnit测试用例的局限性,我们来看一下Mockito如何规避这些缺点,如图所示。
利用Mockito框架提供的强大模拟对象功能,模拟出UserDao对象,并去掉UserDao与DB连接的关系,可以快速地开发出独立、稳定的测试用例,该测试用例不会因为DB异常而导致运行失败。实际中,JUnit和Mockito两者定位不同,项目中通常的做法是联合JUnit +Mockito来进行测试。
上一节,简单了解了Mockito的概念和优点,这里列举几个简单实例来体验一下Mockito。
@Test
public void testMockito_1(){
List mock = mock(List.class);
when(mock.get(0)).thenReturn("ay");
when(mock.get(1)).thenReturn("al");
//测试通过
Assertions.assertThat(mock.get(0)).isEqualTo("ay");
//测试不通过
Assertions.assertThat(mock.get(1)).isEqualTo("xx");
}
上面实例中,使用Mockito模拟List的对象,拥有 List的所有方法和属性。when(xxxx).thenReturn(yyyy)指定当执行了这个方法的时候,返回thenReturn的值,相当于是对模拟对象的配置过程,为某些条件给定一个预期的返回值。Mockito通过when(xxx).thenReturn(yyy)这样的语法来定义对象方法和参数(输入),然后在thenReturn中指定结果(输出),此过程称为stub打桩。一旦这个方法被stub了,就会一直返回这个stub的值。
stub打桩时,需要注意以下几点:
首先,我们开发AyUser实体、UserDao和UserService接口、UserServiceImpl实现类,具体代码如下:
//用户实体
public class AyUser{
private Integer id;
private String name;
public AyUser(Integer id, String name){
this.id = id;
this.name = name;
}
public AyUser(){}
}
// UserDao
@Component
public class UserDao{
public AyUser findUser(Integer userId){
AyUser user = null; // 查询数据库
return user;
}
public boolean deleteUser(Integer userId){
//操作数据库
return true;
}
}
// 用户接口
public interface UserService{
//查询用户
AyUser findUser(Integer id);
//删除用户
boolean deleteUser(Integer id);
}
// UserServiceImple实现类
@Component
public class UserServiceImpl implements UserService{
@Resource
private UserDao userDao;
@Override
public AyUser findUser(Integer id) {
AyUser ayUser = userDao.findUser(id);
return ayUser;
}
@Override
public boolean deleteUser(Integer id){
boolean isSuccess = userDao.deleteUser(id);
return isSuccess;
}
}
然后,我们开发测试用例,具体代码如下:
@Test
public void testMockito_2(){
UserService userService = mock(UserServiceImp1.class);
when(userService.findUser(1)).thenReturn(new AyUser(1, "ay));
//通过mock,查询出模拟用户对象
AyUser ayUser = userService.findUser(1);
//删除用户
boolean isSuccess userService.deleteUser(ayUser.getId());
Assertions.assertThat(isSuccess).isFalse();
}
在testMockito_2测试用例方法中,当mock对象UserServiceImpl查询用户的时候返回mock对象new AyUser(1,“ay”),最后删除用户对象。本节列举的实例非常简单,更多Mockito资料请参考官方文档(https://static.javadoc.io/org.mockito/mockito-core/2.25.0/org/mockito/Mockito.html)。读者可根据官方文档,编写出适合自己业务需求的测试用例,在之后的工作中,可以使用该测试框架模拟依赖,简化单元测试中复杂的依赖关系。
Mockito由于其可以极大地简化单元测试的书写过程而被许多人应用在自己的工作中,但是Mockito工具不可以实现对静态函数、构造函数、私有函数、Final函数以及系统函数的模拟,但是这些方法往往是我们在大型系统中需要的功能。
PowerMock就是在Mockito基础上扩展而来,通过定制类加载器等技术,PowerMock实现了上述所有模拟功能,使其成为分布式微服务架构必备的单元测试工具。
PowerMock有两个重要的注解:
如果测试用例里没有使用注解@PrepareForTest,那么可以不用加注解@RunWith(PowerMockRunner.class),反之亦然。当需要使用PowerMock的强大功能(Mock静态、final、私有方法等)的时候,就需要加注解@PrepareForTest。使用PowerMock之前,需要在项目的pom.xml文件中添加依赖信息,具体代码如下:
<properties>
<org.powermock.version>1. 7.0org.powermock.version>
properties>
<dependency>
<groupId>org.powermockgroupId>
<artifactId>powermock-api-mockitoartifactId>
<scope>testscope>
<version>${org.powermock.version}version>
dependency>
<dependency>
<groupId>org.powermockgroupId>
<artifactId>powermock-module-junit4artifactId>
<scope>testscope>
<version>${org.powermock.version}version>
dependency>
接下来,我们来看具体的实例:
public class PowerMockioTest{
Logger logger = LoggerFactory.getLogger(PowerMockioTest.class);
@Test
public void testFindUser() throws Exception{
//mock对象
UserService userService = PowerMockito.spy(new UserService());
//设置MAX_TIME = 100
Whitebox.setInternalState(userService, "MAX_TIME", new AtomicInteger(100));
String name = "ay";
//模拟调用getUserFromDB方法, 返回new User(1,"ay")对象
PowerMockito.when(userService.getUserFromDB()).thenReturn(new User(1,"ay"));
Assert.assertEquals(userService.findUser("ay").getName(),"ay");
Whitebox.setInternalState(userService, "MAX_TIME", new AtomicInteger(130));
try{
//调用findUser方法
PoerMockito.when(userService, "findUser", name);
}catch(Exception e){
logger.error(e.getMessage());
}
}
}
// 用户服务
class UserService{
Logger logger = LoggerFactory.getLogger(UserService.class);
// 当前调用次数
public AtomicInteger MAX_TIME;
public User findUser(String name) throws Exception{
//findUser方法一天只能调用120次
if (MAX_TIME.get() > 120) {
throw new Exception("系统繁忙");
}
//模拟从数据库中查询到的数据
User user = getUserFromDB();
Integer maxTime = MAX_TIME.getAndIncrement();
//记录日志
logger.info("the current time is :" + maxTime);
return user;
}
public AtomicInteger getMAX_TIME(){
return MAX_TIME;
}
public void setMAX_TIME(AtomicInteger MAX_TIME){
this.MAX_TIME = MAX_TIME;
}
public User getUserFromDB(){
return new User(1, "al");
}
}
class User{
private Integer id;
private String name;
//省略get和set
}
上述实例中,PowerMockito.spy用来模拟对象,Whitebox.setInternalState用来模拟给对象设置值,PowerMockito.when用来模拟方法内部的逻辑。
H2是一个开源的、内存型嵌入式(非嵌入式设备)数据库引擎,它是一个用Java开发的类库,可直接嵌入到应用程序中,与应用程序一起打包发布,不受平台限制。更多H2的资料请参考官方文档(http://www.h2database.com/html/tutorial.html)。
<dependency>
<groupId>com.h2databasegroupId>
<artifactId>h2artifactId>
<scope>runtimescope>
dependency>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactid>spring-starter-boot--data-jpaartifactId>
dependency>
<dependency>
<groupId>org.projectlombokgroupId>
<artifactId>lombokartifactId>
dependency>
Lombok能通过注解的方式,在编译时自动为属性生成构造器、getter/setter、equals、hashcode、toString等方法。在源代码中没有getter和setter方法,但是在编译生成的字节码文件中有getter和setter方法。这样就省去了手动重建这些代码的麻烦,使代码看起来更简洁。
### 是否生成ddl语句
spring.jpa.generate-ddl=false
### 是否打印sq1语句
spring.jpa.show-sql=true
### 自动生成ddl,由于指定了具体的ddl,此处设置为none
spring.jpa.hibernate.ddl-auto=none
### 使用H2数据库
spring.datasource.platform=h2
## H2驱动
spring.datasource.driverclassName =org.h2.Driver
### 指定生成数据库的schema文件位置
spring.datasource.schema=classpath:/db/schema.sql
### 指定插入数据库语句的脚本位置
spring.datasource.data=classpath:/db/data.sql
schema.sql文件内容如下:
CREATE TABLE `ay_user`(
`id` bigint(11) unsigned NOT NULL AUTO _INCREMENT,
`name` varchar(11) DEFAULT NULL,
`url` varchar(200) DEFAULT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
data.sql文件内容如下:
INSERT INTO ay_user (id, name, url) VALUES (1, 'ay', 'https://huangwenyi. com');
INSERT INTO ay_user (id, name, url) values (2, 'al', 'https://al.com');
上述代码中,我们创建了用户表ay_user,同时往表里插入2条数据。随着项目启动,数据初始化到内存中,停止项目,数据消失。
@Repository
public interface Userepository extends JpaRepository<User, Long> {
User findByName(String name);
}
@Entiry
@Table(name = "ay_user")
@Data
public class User {
@Id
@GeneratedValue(Strategy = GenerationType.IDENTITY)
private Long id;
private String name;
private String url;
}
上述代码中,我们创建了ay_user表对应的实体类User,同时开发了UserRepository类,用来与H2数据库交互,查询数据。类中定义了findByName方法,作用是通过用户名查询用户。
在测试类中开发测试用例,具体代码如下:
@RunWith(SpringRunner.class)
@SpringBootTest
@TestPropertySource("classpath:application-test.properties")
public class DemoApplicationTests{
@Test
public void contextLoads(){}
@Resource
private UserRepository userRepository;
@Test
public void testSave() throws Exception{
User user = new User();
user.setName("ay");
user.setUrl("https://huangwenyi.com");
User result = userRepository.save(user);
Assertions.assertThat(result.isNotNull());
}
@Test
public void testFindone() throws Exception{
User user = userRepository.findById(1L).get();
Assertions.assertThat(user).isNotNull();
Assertions.assertThat(user.getId()).isEqualTo(1);
}
@Test
public void testFindByName () throws Exception{
User user = userRepository.findByName("ay");
Assertions.assertThat(user).isNotNull();
Assertions.assertThat(user.getName()).isEqualTo("ay");
}
}
逐个执行测试用例,查看测试结果。
Postman是一款功能强大的网页调试和模拟发送HTTP请求的Chrome插件,支持几乎所有类型的HTTP请求,操作简单且方便。
接下来,我们学习如何通过Postman测试REST API,具体步骤如下:
@RestController
@Controller
public class AyController{
@RequestMapping ("/say")
public String say(Model model){
return "hello ay";
}
@PostMapping("/save")
public String save (Model model, @RequestBody User user){
System.out.printIn(model)
return"save"+user.name+"success";
}
class User{
private String name;
//省略set、get方法
}
}
Postman软件在工作中经常使用,本节只是简单地带读者入门,更多内容请查询官方文档,地址为https://learning.getpostman.com/docs/postman/launching_postman/installation_and_updates/。
AB是Apache自带的压力测试工具。AB非常实用,它不仅可以对Apache服务器进行网站访问压力测试,也可以对其他类型的服务器进行压力测试。比如Nginx、Tomcat、IIS等。
大型互联网项目,用户流量大,基本要求微服务达到三高要求(高性能、高可用和高并发)。因此,我们需要一些测试服务性能的工具来检验微服务的性能,而Apache的AB工具就是一款性能测试的利器,在大型互联网项目中被广泛使用。
执行命令:ab --help,可以查ab命令参数的详细信息。
ab命令提供的参数很多,一般使用-c和-n参数就基本够用了。例如:
从输出的信息可以看出,百度网站首页的吞吐量为16.24个/s,平均响应时间是123.115ms。
上述只是一个简单的实例,具体性能测试需要根据业务需求具体分析。