深入解析JAVA的单元测试和集成测试
Java
测试要求
测试要求来源于阿里嵩山版Java开发手册:
- 【强制】好的单元测试必须遵守 AIR 原则。
说明:单元测试在线上运行时,感觉像空气(AIR)一样感觉不到,但在测试质量的保障上,却是非常关键的。好的单元测试宏观上来说,具有自动化、独立性、可重复执行的特点。
⚫ A:Automatic(自动化)
⚫ I:Independent(独立性)
⚫ R:Repeatable(可重复) - 【强制】单元测试应该是全自动执行的,并且非交互式的。测试用例通常是被定期执行的,执 行过程必须完全自动化才有意义。输出结果需要人工检查的测试 不是一个好的单元测试。单元测试中不准使用 System.out 来进行人肉验证,必须使用 assert 来验证。
- 【强制】保持单元测试的独立性。为了保证单元测试稳定可靠且便于维护,单元测试用例之间决不能互相调用,也不能依赖执行的先后次序。
反例:method2 需要依赖 method1 的执行,将执行结果作为 method2 的输入。 - 【强制】单元测试是可以重复执行的,不能受到外界环境的影响。
说明:单元测试通常会被放到持续集成中,每次有代码 check in 时单元测试都会被执行。如果单测对外部 环境(网络、服务、中间件等)有依赖,容易导致持续集成机制的不可用。
正例:为了不受外界环境影响,要求设计代码时就把 SUT 的依赖改成注入,在测试时用 spring 这样的 DI框架注入一个本地(内存)实现或者 Mock 实现。 - 【强制】对于单元测试,要保证测试粒度足够小,有助于精确定位问题。单测粒度至多是类级 别,一般是方法级别。
说明:只有测试粒度小才能在出错时尽快定位到出错位置。单测不负责检查跨类或者跨系统的交互逻辑, 那是集成测试的领域。 - 【强制】核心业务、核心应用、核心模块的增量代码确保单元测试通过。
说明:新增代码及时补充单元测试,如果新增代码影响了原有单元测试,请及时修正。 - 【强制】单元测试代码必须写在如下工程目录:src/test/java,不允许写在业务代码目录下。
说明:源码编译时会跳过此目录,而单元测试框架默认是扫描此目录。 - 【推荐】单元测试的基本目标:语句覆盖率达到 70%;核心模块的语句覆盖率和分支覆盖率都 要达到 100%
说明:在工程规约的应用分层中提到的 DAO 层,Manager 层,可重用度高的 Service,都应该进行单元测试。 - 【推荐】编写单元测试代码遵守 BCDE 原则,以保证被测试模块的交付质量。
⚫ B:Border,边界值测试,包括循环边界、特殊取值、特殊时间点、数据顺序等。
⚫ C:Correct,正确的输入,并得到预期的结果。
⚫ D:Design,与设计文档相结合,来编写单元测试。
⚫ E:Error,强制错误信息输入(如:非法数据、异常流程、业务允许外等),并得到预期的结果。
10.【推荐】对于数据库相关的查询,更新,删除等操作,不能假设数据库里的数据是存在的,或 者直接操作数据库把数据插入进去,请使用程序插入或者导入数据的方式来准备数据。
反例:删除某一行数据的单元测试,在数据库中,先直接手动增加一行作为删除目标,但是这一行新增数 据并不符合业务插入规则,导致测试结果异常。
11.【推荐】和数据库相关的单元测试,可以设定自动回滚机制,不给数据库造成脏数据。或者对单元测试产生的数据有明确的前后缀标识。
正例:在阿里巴巴企业智能事业部的内部单元测试中,使用 _ENTERPRISE_INTELLIGENCE UNIT_TEST _的前缀来标识单元测试相关代码。
12.【推荐】对于不可测的代码在适当的时机做必要的重构,使代码变得可测,避免为了达到测试 要求而书写不规范测试代码。
13.【推荐】在设计评审阶段,开发人员需要和测试人员一起确定单元测试范围,单元测试最好覆盖所有测试用例(UC)。
14.【推荐】单元测试作为一种质量保障手段,在项目提测前完成单元测试,不建议项目发布后补 充单元测试用例。
15.【参考】为了更方便地进行单元测试,业务代码应避免以下情况:
⚫ 构造方法中做的事情过多。
⚫ 存在过多的全局变量和静态方法。
⚫ 存在过多的外部依赖。
⚫ 存在过多的条件语句。
说明:多层条件语句建议使用卫语句、策略模式、状态模式等方式重构。
16.【参考】不要对单元测试存在如下误解:
⚫ 那是测试同学干的事情。本文是开发手册,凡是本文内容都是与开发同学强相关的。
⚫ 单元测试代码是多余的。系统的整体功能与各单元部件的测试正常与否是强相关的。
⚫ 单元测试代码不需要维护。一年半载后,那么单元测试几乎处于废弃状态。
⚫ 单元测试与线上故障没有辩证关系。好的单元测试能够最大限度地规避线上故障。
命名规格
- 类命名:Test,以它要测试的类的名称开始,以 Test 结尾。例如,OrderInfoTest
- 方法命名:test,其中****是要测试的方法的名称。例如,testCalculateTotal()
- 路径:使用test作为*包名
单元测试
定义:是指对软件中的最小可测试单元进行检查和验证。
Java里单元指一个方法。单元测试是在软件开发过程中要进行的最低级别的测试活动,软件的独立单元将在与程序的其他部分相隔离的情况下进行测试。
断言
断言(Assertion)是测试中常用的一种技术,用于验证测试的预期结果是否与实际结果相符。断言通常用于测试框架中,用于检查代码的输出、行为或状态是否符合预期。在Java中,常见的断言工具是JUnit框架提供的断言方法,例如assertEquals()、assertTrue()、**assertFalse()**等。这些方法接受一个预期值和一个实际值,如果两者相符,则测试通过;否则,测试失败并抛出异常。
//1、assertEquals(expected, actual):验证预期值和实际值是否相等。
assertEquals(10, result); // 预期值为10,实际值为result
//2、assertTrue(condition):验证条件是否为真。
assertTrue(result > 0); // 验证result大于0
//3、assertFalse(condition):验证条件是否为假
assertFalse(result.isEmpty()); // 验证result不为空
//4、assertNull(object):验证对象是否为null
assertNull(result); // 验证result为null
//5、assertNotNull(object):验证对象是否不为null。
assertNotNull(result); // 验证result不为null
在Java中,也可以自定义断言来满足特定的测试需求。自定义断言可以根据你的应用程序逻辑进行定制化,以便更好地验证测试的预期结果。
//1、创建一个包含静态方法的类,用于执行自定义断言逻辑。
public class CustomAssertions {
public static void assertEvenNumber(int number) {
if (number % 2 != 0) {
throw new AssertionError("Expected an even number, but got: " + number);
}
}
}
//2、在测试代码中使用自定义断言方法。
@Test
public void testCustomAssertion() {
int result = 10;
CustomAssertions.assertEvenNumber(result);
}
//上述代码中自定义断言方法assertEvenNumber()用于验证给定的数字是否为偶数。如果数字不是偶数,断言会抛出AssertionError异常,测试将失败
SpringBootTest框架
Spring Boot Test框架是基于JUnit的测试框架,是用于编写和执行单元测试、集成测试和端到端测试的测试框架,专门针对Spring Boot应用程序进行测试。框架提供了一系列注解、类和工具,以简化测试的编写和执行过程,并提供了与Spring应用程序的集成和自动化配置
以下是Spring Boot Test框架的一些关键特点和功能:
- 自动配置:Spring Boot Test框架基于Spring Boot的自动配置原理,可以自动配置测试环境,包括数据库、缓存、消息队列等。
- 注解驱动:Spring Boot Test框架使用注解来标记和配置测试类和方法,例如**@SpringBootTest**、@RunWith、@DataJpaTest等。
- 依赖注入:Spring Boot Test框架支持依赖注入,可以使用**@Autowired**注解注入需要测试的对象或模拟对象。
- 模拟对象:Spring Boot Test框架与Mockito和EasyMock等模拟对象框架集成,方便创建和操作模拟对象。
- 集成测试:Spring Boot Test框架提供了对Web应用程序的集成测试支持,可以模拟HTTP请求和响应,并进行端到端的测试。
- 数据库测试:Spring Boot Test框架支持数据库相关测试,包括内存数据库的自动配置和管理,以及数据访问层的单元测试和集成测试。
- 测试工具:Spring Boot Test框架提供了一些测试工具类和辅助方法,例如TestRestTemplate用于发送HTTP请求,TestEntityManager用于操作JPA实体管理器等。
所需依赖
// 基础依赖 spring-boot-starter-test中包含了junit和mockito等依赖
// pom.xml
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
// gradle
testCompile 'org.springframework.boot:spring-boot-starter-test'
//还可以根据需要添加其他测试相关的依赖项,并确保它们与Spring Boot版本兼容
常用注解
@SpringBootTest注解:
该注解用于标记测试类,指示Spring Boot在测试环境中加载完整的应用程序上下文,注解用于在Spring Boot的测试环境中加载完整的应用程序上下文。它会根据配置文件和注解自动配置应用程序的各个组件,并创建一个与实际运行环境相似的测试环境。通过加载完整的应用程序上下文,可以进行更全面的集成测试,包括对依赖关系的正确性和整个应用程序的功能进行验证@RunWith:
该注解用于指定测试运行器,用于执行测试。在Spring Boot中通常使用**@RunWith(SpringRunner.class)**。
1. **BlockJUnit4ClassRunner**:默认的运行器,用于运行基于 JUnit 4 的测试类。
2. **PowerMockRunner**:用于支持使用 PowerMock 框架进行单元测试,可以模拟静态方法、私有方法等
3. **AndroidJUnit4**:用于在 Android 环境中运行测试,支持 Android 特定的功能和断言
4. **SpringRunner** 是 JUnit 4 的一个运行器(Runner),用于在 Spring 环境中运行测试,继承自 JUnit 的 **BlockJUnit4ClassRunner**,它提供了与 Spring 框架集成的功能。当使用 **SpringRunner** 运行测试类时,会自动创建和管理 Spring 容器,并将依赖注入到测试类中,使得可以在测试中使用 Spring 的功能,如依赖注入、事务管理、AOP 等
@MockBean: 该注解用于创建一个模拟对象(Mock)并将其注入到应用程序上下文中。它通常用于模拟依赖的外部组件或服务
@Autowired: 该注解用于自动注入依赖。它可以用于将被测试对象或其他组件注入到测试类中。@Test:
该注解用于标记测试方法。测试方法应该使用该注解进行注释,以便在执行测试时被识别。@Test(timeout = 1000)
测试方法执行超过1000毫秒后算超时,测试将失败。@Test(expected = Exception.class)
测试方法期望得到的异常类,如果方法执行没有抛出指定的异常,则测试失败。@Before:
该注解用于标记在每个测试方法之前执行的方法。它通常用于准备测试环境,例如初始化对象或设置测试数据@After:
该注解用于标记在每个测试方法之后执行的方法。它通常用于清理测试环境,例如释放资源或重置状态
测试案例
@RunWith(SpringRunner.class) // 使用Spring环境测试
@SpringBootTest //指示Spring Boot在测试环境中加载完整的应用程序上下文(整个应用程序的配置和运行环境)
public class Test {
@Autowired
private MyService myService;
@Test
public void testAddNumbers() {
int result = myService.addNumbers(2, 3);
assertEquals(5, result);
}
}
import net.remote.response.AccountApiResponse;
import net.service.UserService;
import net.site.SiteInfoService;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.Mockito;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.boot.test.mock.mockito.MockBean;
import org.springframework.test.context.junit4.SpringRunner;
@RunWith(SpringRunner.class)
@SpringBootTest
@EnableAutoConfiguration
public class SpBootMokDemo{
@Autowired
private SiteInfoService siteInfoService;
@MockBean
private UserService userService;
@Before
public void setup() throws Exception {
// 模拟方法调用的返回值
AccountApiResponse.AccountDetailReturn accountDetailReturn = new AccountApiResponse.AccountDetailReturn();
accountDetailReturn.setStatus("okk");
Mockito.when(userService.getUserInfoById("aaa")).thenReturn(accountDetailReturn); //模拟方法 参照PowerMock测试框架
}
@Test
public void testCase()throws Exception {
System.out.println(userService.getUserInfoById("aaa"));
}
}
Mocking
Mocking是软件开发中的一种技术,用于模拟或替代系统中的依赖项,以便进行单元测试。通过模拟依赖项的行为,我们可以控制测试环境并隔离被测代码的影响。在进行单元测试时,我们可以使用mocking框架创建和配置模拟对象,以替代实际的依赖项。通过定义模拟对象的行为和返回值,我们可以模拟依赖项的响应,以便在测试中验证被测代码的行为。
Mocking通常用于以下情况:
- 当某个依赖项不容易创建或访问,例如数据库连接、网络请求或外部服务。
- 当某个依赖项的行为不稳定或不可靠,例如随机生成的数据或时间相关的操作。
- 当某个依赖项的使用会导致副作用,例如修改数据库或发送电子邮件。
常见的mocking框架包括:
- Mockito:用于Java的常见mocking框架,易于使用且功能强大。
- PowerMock:在Mockito基础上扩展,支持更多的mocking场景,如静态方法、私有方法和构造函数等。
- EasyMock:另一个流行的Java mocking框架,提供了简单的API和易于理解的语法。
- Jest:用于JavaScript的mocking框架,适用于前端开发和Node.js环境。
- Sinon.js:JavaScript的另一个流行mocking框架,可以与Jest等测试框架集成使用。
使用这些mocking框架,可以轻松地创建模拟对象,并使用断言和验证来验证被测代码与依赖项之间的交互。这样就可以更好地控制测试环境,减少外部因素对测试结果的影响,并提高测试的可靠性和可重复性
Mockito框架
Mockito是一个用于Java的流行的mocking框架,用于进行单元测试和行为驱动开发(BDD)。它提供了简洁而强大的API,使开发人员可以轻松地创建和配置模拟对象,并验证被测代码与依赖项之间的交互。
所需依赖
// 1、添加Spring Boot Starter Test依赖,含了JUnit和Mockito的依赖会自动引入JUnit和Mockito无需单独添加它们的依赖
//在测试类中使用@RunWith(SpringRunner.class)注解,可与Spring Boot集成测试一起使用Mockito。可以使用Mockito来模拟Bean与注入依赖
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
//2、引入Mockito的核心库
//这只是Mockito的核心依赖,如果需要使用Mockito的其他功能或扩展,可能需要添加其他相关依赖
<dependency>
<groupId>org.mockito</groupId>
<artifactId>mockito-core</artifactId>
<version>3.11.2</version>
<scope>test</scope>
</dependency>
//除了Mockito本身的依赖,还需要确保项目中包含JUnit或其他适当的测试框架的依赖,因为Mockito通常与测试框架一起使用
//添加JUnit的依赖 , 引入整个 JUnit Jupiter 模块
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter</artifactId>
<version>5.8.1</version>
<scope>test</scope>
</dependency>
常用方法
Mockito.mock(classToMock) //模拟对象 可以使用@Mock注解代替
Mockito.verify(mock) //验证行为是否发生
Mockito.when(methodCall).thenReturn(value1).thenReturn(value2) //触发时第一次返回value1,第n次都返回value2
Mockito.doThrow(toBeThrown).when(mock).[method] //模拟抛出异常。
Mockito.mock(classToMock,defaultAnswer) //使用默认Answer模拟对象
Mockito.when(methodCall).thenReturn(value) //参数匹配
Mockito.doReturn(toBeReturned).when(mock).[method] //参数匹配(直接执行不判断)
Mockito.when(methodCall).thenAnswer(answer)) //预期回调接口生成期望值
Mockito.doAnswer(answer).when(methodCall).[method] //预期回调接口生成期望值(直接执行不判断)
Mockito.spy(Object) //用spy监控真实对象,设置真实对象行为
Mockito.doNothing().when(mock).[method] //不做任何返回
Mockito.doCallRealMethod().when(mock).[method] //等价于Mockito.when(mock.[method]).thenCallRealMethod(); 调用真实的方法
reset(mock) //重置mock
//Mockito继承Matchers,anyInt()等均为Matchers方法,使用了参数匹配,那么所有的参数都必须通过matchers来匹配,当传入两个参数
Mockito.anyInt() //任何 int 值 ;
Mockito.anyLong() //任何 long 值 ;
Mockito.anyString() //任何 String 值 ;
Mockito.anyBoolean() //任何boolean 值
Mockito.any(XXX.class) //任何 XXX 类型的值 等等
Mockito.any() //任何参数
使用案例
//验证行为是否发生
List mock = Mockito.mock(List.class);//模拟创建一个List对象
mock.add(1);//调用mock对象的方法
mock.clear();
Mockito.verify(mock).add(1);//验证方法是否执行
//多次触发返回不同值
Iterator iterator = mock(Iterator.class);//mock一个Iterator类
Mockito.when(iterator.next()).thenReturn("hello").thenReturn("world");//预设当iterator调用next()时第一次返回hello,第n次都返回world
String result = iterator.next() + " " + iterator.next() + " " + iterator.next();//使用mock的对象
Assert.assertEquals("hello world world",result);//验证结果
//模拟抛出异常
@Test(expected = IOException.class)//期望报IO异常
public void when_thenThrow() throws IOException{
OutputStream mock = Mockito.mock(OutputStream.class);
Mockito.doThrow(new IOException()).when(mock).close();//预设当流关闭时抛出异常
mock.close();
}
//参数匹配
@Test
public void with_arguments(){
TestB b = Mockito.mock(TestB.class);
Mockito.when(b.getSex(1)).thenReturn("男");//预设根据不同的参数返回不同的结果
Mockito.when(b.getSex(2)).thenReturn("女");
Assert.assertEquals("男", b.getSex(1));
Assert.assertEquals("女", b.getSex(2));
Assert.assertEquals(null, b.getSex(0));//对于没有预设的情况会返回默认值
}
@Data
class TestB{
private String name;
public String getSex(Integer sex){
if(sex==1){
return "man";
}else{
return "woman";
}
}
}
// 匹配参数
@Before
public void setup() throws Exception{
// 模拟鉴权方法调用的返回值 : 输入任意参数 返回 true
Mockito.when(accountService.isMain(Mockito.any())).thenReturn(true);
// 模拟云盘剩余额度方法请求的返回值 : 输入任意参数 返回 10086
Mockito.when(rulesService.getAllowSSDVolumeSize(Mockito.any(),Mockito.any())).thenReturn(10086);
// 模拟实例剩余额度方法请求的返回值 : 输入任意参数 返回 10086
Mockito.when(rulesService.getAllowCreateVmNum(Mockito.anyBoolean(),Mockito.any(),Mockito.any())).thenReturn(10086);
}
//重置 mock
@Test
public void reset_mock(){
List list = mock(List.class);
Mockito. when(list.size()).thenReturn(10);
list.add(1);
Assert.assertEquals(10,list.size());
//重置mock,清除所有的互动和预设
Mockito.reset(list);
Assert.assertEquals(0,list.size());
}
@Mock注解
@Mock注解是Mockito框架提供的一个注解,用于创建一个模拟对象(Mock Object)。可以理解为对 mock 方法的一个替代。使用该注解时,要使用MockitoAnnotations.initMocks 方法,让注解生效。旧版的是initMocks,新版的是openMocks(从Mockito版本3.4.0开始引入的新方法)。也可以用MockitoJUnitRunner来代替MockitoAnnotations.initMocks
import org.junit.Assert;
import org.junit.Before;
import org.junit.Test;
import org.mockito.Mock;
import org.mockito.MockitoAnnotations;
import java.util.Random;
import static org.mockito.Mockito.*;
public class MockitoDemo {
@Mock
private Random random;
@Before
public void before() {
// 让注解生效
MockitoAnnotations.initMocks(this);
}
@Test
public void test() {
when(random.nextInt()).thenReturn(100);
Assert.assertEquals(100, random.nextInt());
}
}
// 也可以用MockitoJUnitRunner来代替MockitoAnnotations.initMocks
import org.junit.Assert;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.Mock;
import org.mockito.junit.MockitoJUnitRunner;
import java.util.Random;
import static org.mockito.Mockito.*;
@RunWith(MockitoJUnitRunner.class)
public class MockitoDemo {
@Mock
private Random random;
@Test
public void test() {
when(random.nextInt()).thenReturn(100);
Assert.assertEquals(100, random.nextInt());
}
}
使用**@Mock注解和Mockito.mock(classToMock)方法都可以创建模拟对象,它们的功能是相同的。它们都可以用于模拟依赖对象,以便进行单元测试时进行行为验证和结果验证。@MockBean注解是Spring Boot框架提供的注解,它是基于Mockito的@Mock注解的增强版。@MockBean注解除了创建模拟对象外,还会将模拟对象注入到Spring应用程序上下文中,替换实际的bean。这样,在进行集成测试时,可以模拟和控制应用程序中的依赖对象。
@Mock注解和Mockito.mock(classToMock)**之间存在的差异:
- 语法:@Mock注解是一种基于注解的方式,可以直接在测试类中使用。而**Mockito.mock(classToMock)**是一种方法调用,需要在测试方法中显式地调用。
- 初始化:使用**@Mock注解时,通常需要配合MockitoAnnotations.initMocks(this)或MockitoJUnitRunner来初始化模拟对象。而使用Mockito.mock(classToMock)**方法时,模拟对象会立即被创建。
- 可读性:@Mock注解的使用可以更清晰地表达测试类中哪些对象是模拟对象,使代码更易读。而使用**Mockito.mock(classToMock)**方法则需要显式地指定模拟对象的创建。
上一篇:
Java单元测试怎么做?
下一篇:
程序员的21次心碎时刻