项目测试
项目测试是对项目的需求和功能进行测试,由测试人员写出完整的测试用例,再按照测试用例执行测试。项目测试是项目质量的保证,项目测试质量直接决定了当前项目的交付质量。
测试人员在开展测试之前,首先需要进行测试的需求分析,测试需求分析包括:
测试内容:需要进行哪些方面的测试,包括功能测试、性能测试、可靠性测试、易用性测试和安全性测试等;
测试环境:测试环境的配置;
测试工具:选择测试工具,包括缺陷管理工具和自动化测试工具等;
测试轮数:包括冒烟测试、功能测试、并发测试和弱网测试等。
测试需求分析完成后再开始项目测试。测试的步骤包括:冒烟测试、单元测试、集成测试、性能测试等,最后由产品经理完成产品的验收。
单元测试
项目测试的第一步就是单元测试,单元测试也称为模块测试或组件测试。在项目开发过程中,单元测试用来检查项目的单个单元或模块是否正常工作,它是由开发人员在开发后立即在开发环境中完成的。
为什么要做单元测试
单元测试通常是软件测试中基础的测试类型,用于测试单独模块的功能是否有错。它与功能测试的不同之处是,单元测试更加关注的是代码内部的逻辑,而非功能的完整性。
根据上述描述,在项目中实施单元测试有以下5个目标:
隔离各部分代码的功能;
确保单个模块功能逻辑的正确性;
在开发的过程中及时发现代码缺陷并修改;
发现产品开发早期逻辑中的Bug,以降低测试成本;
允许开发人员后续重构或升级代码。
基于单元测试的目标,在项目中使用单元测试有以下4个优点:
能在产品开发周期的早期发现问题,可以大大地降低测试成本,因为早发现一个缺陷的成本要比晚期发现它的成本低得多。
在改变现有功能(回归测试)的同时,可以减少缺陷。
简化了调试过程(测试驱动开发就是基于测试用例来完成功能开发)。调试是在程序中发现并解决妨碍软件正确运行缺陷的过程。当进行单元测试时,如果发现测试失败,则只需要在调试代码中做一下的更改,就可以快速定位错误。
进行单元测试,能够保证代码质量。
单元测试有哪些内容
单元测试的内容主要包括以下两点:
单元测试的方法:通常使用白盒测试;
单元测试的类型:可以选择手动测试或自动测试。
在对代码进行单元测试时,可以使用手动的方式,也可以使用一些自动化工具。手动测试和自动化测试的区别主要体现在执行效率和操作等方面,如表7.1所示。
在实际开发中流行DevOps(Development+Operations)开发模式,因此建议读者在项目中能使用自动化测试完成的任务尽量采用自动化测试来完成,以提升开发效率。
想要精通单元测试,还需要了解单元测试的几个关键点:
(1)执行单元测试的时间:一般在开发完成后立即进行。
(2)谁做单元测试:开发人员进行自测。
(3)明确单元测试的具体任务,包括两个方面:
首先,准备单元测试的计划,包括:准备测试计划;
回顾测试计划;
修订测试计划;
定义单元测试计划的基准数据。
其次,准备测试用例和脚本,包括:
准备测试环境、测试用例和脚本;
回顾测试用例和脚本;
修订测试用例和脚本。
(4)定义单元测试用例和脚本的基准数据。
(5)执行单元测试,完成后出具单元测试报告。
常规的JUnit测试
JUnit是Java应用开发中使用最广泛的单元测试框架。因为Java 8发布了Lambda表达式,使得Java的编码风格发生了巨大的变化,所以JUnit团队适时推出了新的框架——JUnit 5。JUnit 5能够适应Lambda风格的编码,建议在JDK 1.8及之后版本的项目中使用JUnit 5来创建和执行单元测试。本书中的单元测试以JUnit 5为例。
至于什么是JUnit,看以下官方的定义:
JUnit 5 is the next generation of JUnit. The goal is to create
an up-to-date foundation for developer-side testing on the JVM.
This includes focusing on Java 8 and above, as well as enabling
many different styles of testing.
官方提示JUnit 5是新一代的JUnit,它的目标是为JVM上的开发人员做测试创建一个最新的基础,为Java 8及以上版本创建不同的测试风格。JUnit5框架=JUnit Platform+JUnit Jupiter+JUnit Vintage,其各部分框架的含义如下:
JUnit Platform是在JVM上启动测试框架的基础。
JUnit Jupiter是JUnit 5扩展的新的编程模型和扩展模型,用来编写测试用例。Jupiter子项目为在平台上运行Jupiter的测试提供了一个TestEngine(测试引擎)。
JUnit Vintage提供了一个在平台上运行JUnit 3和JUnit 4的TestEngine。
JUnit 5的架构如图7.1所示。
第一层测试用例:开发人员使用junit-jupiter-api等测试框架的API编写业务代码的单元测试。第二层测试引擎,JUnit测试框架实现引擎API的框架,jupiterengine和vintage- engine分别是JUnit 4和JUnit 5对测试引擎API的实现。
第三层junit-platform-engine:junit-platform-engine平台引擎是对第二层中两种不同引擎实现的抽象,是测试引擎的接口标准。
第四层IDEA:启动器通过ServiceLoader发现测试引擎的实现并安排其执行。它为IDE和构建工具提供了API接口,因此在IDE中可以直接执行测试,例如,启动测试并显示其结果。
在项目中使用JUint 5进行单元测试时需要用到一些注解,如表7.2所示。此外,表中对JUnit 4和JUnit 5的注解使用进行了对比,供读者参考。
根据以上的注解,下面使用JUint 5完成一个单元测试示例。
(1)新建一个项目,在pom.xml中添加JUnit 5的依赖,代码如下:
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.3.10.RELEASE</version>
<relativePath/> <!-- lookup parent from repository --></parent>
<groupId>com.example</groupId>
<artifactId>junit5-demo</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>junit5-demo</name>
<description>Demo project for Spring Boot</description>
<properties>
<java.version>11</java.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter</artifactId>
</dependency>
<!--加入JUnit 5的版本测试;如果想用JUnit 4进行测试,把exclusions去除-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
(2)完成一些简单的业务代码。新建一个用户服务类的接口
UserService:package com.example.junit5demo.service;
import java.util.IllegalFormatException;
/**
* 测试接口
*/
public interface UserService {
/**
* 登录
* @param userName
* @param password
* @return
* @throws IllegalFormatException
*/
boolean login(String userName,String password) throws
IllegalFormat Exception;
/**
* 查询数量
* @return
*/
int countNum();
}
(3)新建一个类来实现UserService接口,这里使用的是最简单的实现方式,暂时先不连接数据库。
package com.example.junit5demo.service;
/**
* 测试接口的实现
*/
import org.springframework.stereotype.Service;@Service
public class UserServiceImpl implements UserService {
@Override
public boolean login(String userName, String password) throws
Illegal
ArgumentException {
if (userName == null || password == null
|| userName.isEmpty() || password.isEmpty()) {
throw new IllegalArgumentException("不能为空");
}
if ("cc".equals(userName) && "123".equals(password)) {
return true;
}
return false;
}
@Override
public int countNum() {
return 18;
}
}
(4)实现UserService接口的方法后,在类名上右击,依次选择Go To |Test | Create New Test...命令,具体的过程如图7.2和图7.3所示。按照上述操作会跳转到选择测试方法的页面,如图7.4所示。选择要测试的方法,选中写的两个方法,单击OK按钮就能自动生成测试类和要测试的方法。
(5)添加Spring Boot的启动类,代码如下:
package com.example.junit5demo;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
@SpringBootApplication
public class Junit5DemoApplication {
public static void main(String[] args) {
SpringApplication.run(Junit5DemoApplication.class, args);
}
}
(6)修改自动生成的测试类代码,完成两个业务方法的单元测试工作。
完成后的代码如下:
package com.example.junit5demo.service;
import lombok.extern.slf4j.Slf4j;
import org.junit.jupiter.api.*;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import static org.junit.jupiter.api.Assertions.*;
@Slf4j
@SpringBootTest //SpringBot测试类注解
class UserServiceImplTest {
//输入要测试的类
@Autowired
private UserService userService;
//在所有测试方法之前执行
@BeforeAll
public static void beforeAll() {
log.info("before all");
}
//在每个测试方法执行之前执行
@BeforeEach
public void beforeEach() {
log.info("before each");
}
@AfterEach
public void afterEach() {
log.info("after each");
}
@AfterAll
public static void afterAll() {
log.info("after all");
} //测试countNum方法
@Test
void countNum() {
int i = userService.countNum();
assertEquals(18, i);
assertNotEquals(1, i);
}
// 测试login方法
@Test
void login() {
boolean cc1 = userService.login("cc", "123");
assertEquals(cc1, true);
boolean cc2 = userService.login("cc2", "123");
assertEquals(cc2, false);
assertThrows(IllegalArgumentException.class, () ->
userService.login("", "123"));
assertThrows(IllegalArgumentException.class, () ->
userService.login("123", null));
}
}
(7)运行这个测试类,在IDEA的控制台打印结果如图7.5所示。可以看到左侧的两个方法都显示绿色的勾,说明已经通过测试用例。至此,我们已经完成了UserService类中所有方法的单元测试,可以看出,相关代码逻辑正确。
提示:在完成单元测试后,如果开发人员在测试过程中优化了代码,对自己的代码进行了重构,则需要对重构后的代码再次进行单元测试,以确保其逻辑的正确性,千万不能忽略这一点。
Mock测试
Mock测试是指在单元测试的过程中对一些不容易构造的对象模拟一个对象进行使用的过程。一些对象只能在特定的环境中产生,例如HttpServletRequest对象必须从Servlet容器中才能构造出来,ResultSet对象必须依赖JDBC的实现才能构造,ProceedingJoinPoint对象的构建必须依赖AOP的实现。在遇到这种复杂对象的构建时,使用一个虚拟的对象(Mock对象)来替代,使用一个“假”的对象,便于在测试时顺利检测复杂对象(虚拟对象)的使用逻辑,以便快速、准确地测试自己的代码逻辑。
Mock的出现是为了解决不同的单元之间由于耦合而难于开发与测试的问题,因此在单元测试和集成测试中都会用到Mock。Mock最大的作用是把单元测试的耦合分解开,如果你的代码对另一个类或者接口有依赖,则可以排除依赖,只验证所调用依赖的行为。例如,需要测试UserService中的方法,但是它依赖UserDao,这时就直接模拟一个用户的数据库对象,且只测试UserService中的方法,来验证USerService中的逻辑正确与否。
在进行单元测试时,以下的几个场景需要用到Mock对象:
需要将当前被测单元和依赖模块分离,构造一个独立的测试环境,不关注被测单元的依赖对象,而只关注被测单元的功能逻辑。例如,被测代码中需要依赖第三方接口的返回值进行逻辑处理,可能因为网络或者其他环境因素,调用第三方平台经常会中断或者失败,而无法对被测单元进行测试,这时就可以使用Mock技术来将被测单元和依赖模块独立开,使得测试可以进行下去。
被测单元依赖的模块尚未开发完成,而被测单元需要依赖模块的返回值进行后续处理。包括前后端分离项目中,后端接口开发完成之前,前端接口需要测试;依赖的上游项目的接口尚未开发完成,需要接口联调测试;service层的代码包含对Dao层的调用,但是Dao层的代码还没完成,需要模拟Dao层的对象。
被测单元依赖的对象较难模拟或者构造比较复杂。例如,HttpServletRequest对象和数据库的连接对象Connection都非常难以构造,则可以直接使用模拟后的对象。
在Java项目开发中有很多Mock框架,常见的有Mockito和PowerMock。
Mockito是一个在项目中最常用的优秀的单元测试Mock框架,它能满足大部分业务的测试要求;PowerMock框架可以解决Mockito框架不能解决的更难的问题,如业务代码中的静态方法、私有方法和Final方法等。PowerMock框架是在EasyMock和Mockito的基础上进行扩展的,它通过提供定制的类进行加载器并进行一些字节码的修改,从而实现更强大的测试功能。
本书使用Java开发中常用的Mockito作为Mock框架。下面介绍如何在Spring Boot项目中使用Mock对象进行测试,从而完成单元测试。
(1)在7.1.3小节中的项目文件pom.xml中添加Mockito依赖,代码如下:
<dependency>
<groupId>org.mockito</groupId>
<artifactId>mockito-core</artifactId>
<version>3.11.2</version>
</dependency>
(2)新建一个产品服务类接口ProductService及其实现类,代码如下:
package com.example.junit5demo.service;
public interface ProductService {
int countNum(); boolean productExists(String name);
}
(3)新建上述ProductService接口的实现类,代码如下:
package com.example.junit5demo.service;
import com.example.junit5demo.dao.ProductDao;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
@Service
public class ProductServiceImpl implements ProductService {
@Autowired
private ProductDao productDao;
@Override
public int countNum() {
return productDao.countNum();
}
@Override
public boolean productExists(String name) {
if (name == null || name.isBlank()) {
return false;
}
return productDao.productExists(name);
}
}
(4)新建一个ProductDao类,直接使用类的实现返回数据,代码如下:
package com.example.junit5demo.dao;
import org.springframework.stereotype.Repository;
import java.util.List;
@Repository
public class ProductDao {
public int countNum() {
return 2;
}
public boolean productExists(String name) {
/**
* 模拟Dao的方法
*/
List<String> apple = List.of("cc","apple", "orgage", "banana");
return apple.contains(name);
}
}
(5)生成ProductService的测试类,代码如下:
package com.example.junit5demo.service;
import com.example.junit5demo.dao.ProductDao;
import org.junit.Assert;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.mockito.MockitoAnnotations;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.context.junit.jupiter.SpringExtension;
import static org.mockito.Mockito.when;// 使用Spring的测试框架
@ExtendWith(SpringExtension.class)
@SpringBootTest
class ProductServiceImplTest {
/**
* 输入要测试的对象
*/
@InjectMocks
ProductServiceImpl productService;
/**
* Mock对象的依赖对象
*/
@Mock
ProductDao productDao;
@BeforeEach
public void setUp() {
/**
* 初始化
*/
MockitoAnnotations.openMocks(this);
}
@Test
void coutNum() {
/**
* 当执行这个方法的时候直接返回5
*/
when(productDao.countNum()).thenReturn(5);
int num = productService.countNum();
/**
* 验证返回值
*/
Assert.assertEquals(num,5);
}
@Test void productExists() {
/**
* 这里本来应该返回true,但是故意设置为false,再查看返回值
*/
when(productDao.productExists("cc")).thenReturn(false);
boolean cc = productService.productExists("cc");
Assert.assertEquals(cc,false);
}
@Test
void productExists3() {
when(productDao.productExists("apple")).thenReturn(false);
boolean cc = productService.productExists("apply");
Assert.assertEquals(cc,false);
}
}
(6)在本次测试用例中,需要测试ProductServiceImpl中的两个业务方法。Product- ServiceImpl依赖ProductDao,这时模拟一个对象到测试对象中,当调用Dao的方法时就会返回设定的值,不会真正地执行Dao,而只测试ProductServiceImpl中的方法。执行本测试类ProductServiceImplTest中的所有测试方法,完成测试后的结果如图7.6所示。可以看到,3个测试用例都已通过,说明已经完成了Mock单元测试。
以上就是使用Mock进行单元测试的过程。Mock测试是单元测试的一大利器,它能帮助开发人员更快地完成单元测试、业务代码的检测和Bug的修复。