什么是单元测试?
单元测试(unit testing),是指对软件中的最小可测试单元进行检查和验证的过程就叫单元测试。
单元测试是开发人员编写的一小段代码,用于检验被测代码的一个很小的、很明确的(代码) 功能是否正确。执行单元测试就是为了证明某段代码的执行结果是否符合我们的预期。如果测试结果符合我们的预期,称之为测试通过,否则就是测试未通过(或者叫测试失败)
Java 中的最小测试单元就到方法了,也就说对方法的测试就是单元测试
单元测试的作用
在没有接触单元测试之前我们是怎么做测试的?一般有三个方法:
方式 | 弊端 |
---|---|
启动整个应用像用户正常操作一样,操作界面调用接口 | 每次测试都需要启动整个项目 |
在代码某个地方写一个临时入口,例如main方法测试 | 用完就删除不然影响项目运行速度或效率 |
利用postman工具调用接口 | 每次测试启动服务 |
在时间允许的情况下,编写单元测试是程序员对代码的自测,这是对自己代码的负责。
写单元测试的两个动机:
- 保证或验证实现功能。
- 保护已经实现的功能不被破坏。
Spring Boot引入单元测试
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
注解的使用
注解 | 作用 |
---|---|
@SpringBootTest | 获取启动类,加载配置,寻找主配置启动类 |
@RunWith(SpringRunner.class) | 让JUnit运行Spring的测试环境,获得Spring环境的上下文的支持 |
@RunWith(SpringRunner.class)
@SpringBootTest
public class ApplicationTest {
}
@SpringBootTest 重要参数
- args
应用程序参数,如:args = “–app.test=one” - classes
Spring Boot应用启动入口类名,该参数不指定时由Spring Boot默认查找。 - webEnvironment
默认情况下@SpringBootTest不会启动服务器。当测试Web应用时,需指定该参数以便加载上下文环境。
WebEnvironment枚举值说明:
- MOCK
默认值,加载WebApplicationContext并提供模拟Web环境。使用此注释时,不会启动嵌入式服务器。 - RANDOM_PORT
启动应用并随机监听一个端口。 - DEFINED_PORT
启动应用并监听自定义的端口(来自application.properties)或使用默认端口8080。 - NONE
ApplicationContext通过使用加载,SpringApplication但不提供任何网络环境(模拟或其他方式)。
工具Junit4
注解的使用
注解 | 作用 |
---|---|
@Test | 编写一般测试用例用 |
@Test(timeout = 1000) | 测试方法执行超过1000毫秒后算超时,测试将失败 |
@Test(expected = Exception.class) | 测试方法期望得到的异常类,如果方法执行没有抛出指定的异常,则测试失败。 |
@Before | 在每个方法测试前执行,一般用来初始化方法(比如我们在测试别的方法时,类中与其他测试方法共享的值已经被改变,为了保证测试结果的有效性,我们会在@Before注解的方法中重置数据) |
@After | 在每个测试方法执行后,在方法执行完成后要做的事情 |
@BeforeClass | 在所有测试方法执行前执行 |
@AfterClass | 在所有测试方法执行后执行 |
@Ignore | 修饰的类或方法会被测试运行器忽略 |
@RunWith | 在 Junit 中有很多个 Runner,他们负责调用你的测试代码,每一个 Runner 都有各自的特殊功能,你根据需要选择不同的 Runner 来运行你的测试代码 |
什么是Mock
在面向对象的程序设计中,模拟对象(英语:mock object)是以可控的方式模拟真实对象行为的假对象。在编程过程中,通常通过模拟一些输入数据,来验证程序是否达到预期结果。
为什么使用Mock对象
使用模拟对象,可以模拟复杂的、真实的对象行为。如果在单元测试中无法使用真实对象,可采用模拟对象进行替代。
spring测试框架提供了两种方式,独立安装和集成Web环境测试(此种方式并不会集成真正的web环境,而是通过相应的Mock API进行模拟测试,无须启动服务器)
Java的Mockito框架
Mockito是一款用于java开发的mock测试框架,用于快速创建和配置mock对象。通过创建外部依赖的 Mock 对象, 然后将此 Mock 对象注入到测试类中,简化有外部依赖的类的测试。
MockMvc的概念
MockMvc是由spring-test包提供,实现了对Http请求的模拟,能够直接使用网络的形式,转换到Controller的调用,使得测试速度快、不依赖网络环境。同时提供了一套验证的工具,结果的验证十分方便。
接口MockMvcBuilder,提供一个唯一的build方法,用来构造MockMvc。主要有两个实现:StandaloneMockMvcBuilder和DefaultMockMvcBuilder。
StandaloneMockMvcBuilder:指定 WebApplicationContext,它将会从该上下文获取相应的控制器并得到相应的 MockMvc
@RunWith(SpringRunner.class)
@SpringBootTest
public class UserControllerTest {
@Autowired
private WebApplicationContext webApplicationContext;
private MockMvc mockMvc;
@Before
public void setUp() throws Exception {
mockMvc = MockMvcBuilders.webAppContextSetup(webApplicationContext).build();
}
DefaultMockMvcBuilder:通过参数指定一组控制器,这样就不需要从上下文获取了
@RunWith(SpringRunner.class)
@SpringBootTest
public class UserControllerTest {
private MockMvc mockMvc;
@Before
public void setUp() throws Exception {
mockMvc = MockMvcBuilders.standaloneSetup(new UserController()).build();
}
}
// ...
import static org.hamcrest.Matchers.containsString;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
@RunWith(SpringRunner.class)
@SpringBootTest
public class UserControllerTest {
@Autowired
private WebApplicationContext webApplicationContext;
private MockMvc mockMvc;
@Before
public void setUp() {
mockMvc = MockMvcBuilders.webAppContextSetup(webApplicationContext).build();
}
@Test
public void getUser() {
mockMvc.perform(get("/v1/users/1")
.accept(MediaType.APPLICATION_JSON_UTF8))
.andExpect(status().isOk())
.andExpect(content().string(containsString("\"name\":\"lyTongXue\"")));
}
}
MockMVC的基本步骤
(1) mockMvc.perform执行一个请求。
(2) MockMvcRequestBuilders.get(“XXX”)构造一个请求。
(3) ResultActions.param添加请求传值
(4) ResultActions.accept设置返回类型
(5) ResultActions.andExpect添加执行完成后的断言。
(6) ResultActions.andDo添加一个结果处理器,表示要对结果做点什么事情,比如处使用print输出整个响应结果信息。
(7) ResultActions.andReturn表示执行完成后返回相应的结果。
方法名 | 描述 |
---|---|
Mockito.mock(classToMock) | 模拟对象 |
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 |
使用断言
简单的断言说明
方法名 | 描述 |
---|---|
assertEquals | 判断两个对象或两个原始类型是否相等 |
assertNotEquals | 判断两个对象或两个原始类型是否不相等 |
assertSame | 判断两个对象引用是否指向同一个对象 |
assertNotSame | 判断两个相关对象是否不指向同一个对象 |
assertTrue | 判断给定的布尔值是否为true |
assertFalse | 判断给定的布尔值是否为false |
assertNull | 判断给定的对象引用是否为null |
assertNotNull | 判断给定的对象引用是否不为null |
单元测试生成插件
TestMe插件可以智能分析被测试类的依赖类,结合Mockito+Junit等单元测试框架,生成Mock/InjectMocks依赖关系,自动生成单元测试类。
下载插件
File——>Settings——>Plugins,搜索TestMe,然后install就好了,插件安装完成后需要重启一下。
我们打开一个类,这个类就是我们即将要作为实验的类。光标定位到代码里,右击鼠标选择Generate…选择TestMe…后弹出Test Class,选择的是Junit4。
单元测试规范
1、单元测试代码必须写在如下工程目录 src/test/java,不允许写在业务代码目录下。
2、单元测试的基本目标:语句覆盖率达到 70%;核心模块的语句覆盖率和分支覆盖率都要达到 100%。
3、在工程规约的应用分层中提到的 DAO 层,Manager 层,可重用度高的 Service,都应该进行单元测试。
4、编写单元测试代码遵守 BCDE 原则,以保证被测试模块的交付质量。
阿里巴巴 Java 开发手册 \9. 【推荐】编写单元测试代码遵守 BCDE 原则,以保证被测试模块的交付质量。
Border:边界值测试,包括循环边界、特殊取值、特殊时间点、数据顺序等;
Correct:正确的输入,并得到预期的结果;
Design:与设计文档相结合,来编写单元测试;
Error:强制错误信息输入(如:非法数据、异常流程、非业务允许输入等),并得到预期的结果;
5、对于数据库相关的查询,更新,删除等操作,不能假设数据库里的数据是存在的, 或者直接操作数据库把数据插入进去,请使用程序插入或者导入数据的方式来准备数据。
6、和数据库相关的单元测试,可以设定自动回滚机制,不给数据库造成脏数据。或者 对单元测试产生的数据有明确的前后缀标识。
7、对于不可测的代码建议做必要的重构,使代码变得可测,避免为了达到测试要求而书写不规范测试代码。
8、在设计评审阶段,开发人员需要和测试人员一起确定单元测试范围,单元测试最好覆盖所有测试用例。
9、单元测试作为一种质量保障手段,不建议项目发布后补充单元测试用例,建议在项目提测前完成单元测试。
10、为了更方便地进行单元测试,业务代码应避免以下情况:
构造方法中做的事情过多
存在过多的全局变量和静态方法
存在过多的外部依赖
存在过多的条件语句。说明:多层条件语句建议使用卫语句、策略模式、状态模式等方式重构
11、不要对单元测试存在如下误解:
那是测试同学干的事情
单元测试代码是多余的。系统的整体功能与各单元部件的测试正常与否是强相关的
单元测试代码不需要维护,一年半载后,那么单元测试几乎处于废弃状态
单元测试与线上故障没有辩证关系,好的单元测试能够最大限度地规避线上故障