作者:京东物流 秦彪
1. 什么是单元测验
(1)单元测验环节:
测验进程依照阶段区分分为:单元测验、集成测验、体系测验、验收测验等。相关意义如下:
1)单元测验: 针对计算机程序模块进行输出正确性查验作业。
2)集成测验: 在单元测验根底上,整合各个模块组成子体系,进行集成测验。
3)体系测验: 将整个交给所触及的协作内容都归入其间考虑,包括计算机硬件、软件、接口、操作等等一系列作为一个全体,查验是否满意软件或需求阐明。
4)验收测验: 在交给或许发布之前对所做的作业进行测验查验。
单元测验是阶段性测验的首要环节,也是白盒测验的一种,该内容的编写与实践能够前置在研制完结,研制在编写业务代码的时分就需求生成对应代码的单元测验。单元测验的发起人是程序规划者,受益人也是编写程序的人,所以关于程序员,十分有必要形成自我约束力,完结根本的单元测验用例编写。
(2)单元测验特征:
由上可知,单元测验其实是针对软件中最小的测验单元来进行验证的。这里的单元便是指相关的功能子集,比方一个办法、一个类等。值得注意的是作为最初级别的测验活动,单元测验验证的方针仅限于当时测验内容,与程序其它部分内容相隔离,总结起来单元测验有以下特征:
1)主要功能是证明编写的代码内容与期望输出共同。
2)最小最初级的测验内容,由程序员自身发起,确保程序根本组件正常。
3)单元测验尽量不要区分类与办法,建议以进程性的办法为测验单位,简单实用高效为方针。
4)不要违背主题,专注于测验一小块的代码,确保根底功能。
5)剥离与外部接口、存储之间的依靠,使单元测验可控。
6)任何时刻任何顺序履行单元测验都需求是成功的。
2. 为什么要单元测验
(1)单元测验意义:
程序代码都是由根本单元不断组合成杂乱的体系,底层根本单元都无法确保输入输出正确性,层级递加时,问题就会不断扩大,直到整个体系崩溃无法运用。所以单元测验的意义就在于确保根本功能是正常可用且安稳的。而关于接口、数据源等原因形成的不安稳因素,是外在原因,不在单元测验考虑范围之内。
(2)运用main办法进行测验:
@PostMapping(value="/save")
public Map<String,Object> save(@RequestBody Student stu) {
studentService.save(stu);
Map<String,Object> params = new HashMap<>();
params.put("code",200);
params.put("message","保存成功");
return params;
}
假如要对上面的Controller进行测验,能够编写如下的代码示例,运用main办法进行测验的时分,先发动整个工程运用,然后编写main办法如下进行拜访,在单步调试代码。
public static void main(String[] args) {
HttpHeaders headers = new HttpHeaders();
headers.setContentType(MediaType.APPLICATION_JSON);
String json = "{"name":"张三","className":"三年级一班","age":"20","sex":"男"}";
HttpEntity<String> httpEntity = new HttpEntity<>(json, headers);
String url = "http://localhost:9092/student/save";
MainMethodTest test = new MainMethodTest();
ResponseEntity<Map> responseEntity = test.getRestTemplate().postForEntity(url, httpEntity, Map.class);
System.out.println(responseEntity.getBody());
}
(3)运用main办法进行测验的缺点:
1)经过编写大量的main办法针对每个内容做打印输出到控制台单调繁琐,不具有优雅性。
2)测验办法不能一同运转,成果需求程序员自己判别正确性。
3)统一且重复性作业应该交给东西去完结。
3. 单元测验结构-JUnit
3.1 JUnit简介
JUnit官网:junit.org/。JUnit是一个用于编写可重复测验的简单结构。它是用于单元测验结构的xUnit体系结构的一个实例。
JUnit的特色:
(1) 针关于Java语言特定规划的单元测验结构,运用十分广泛。
(2) 特定领域的标准测验结构。
(3) 能够在多种IDE开发平台运用,包括Idea、Eclipse中进行集成。
(4) 能够便利由Maven引进运用。
(5) 能够便利的编写单元测验代码,检查测验成果等。
JUnit的重要概念:
名称 | 功能作用 |
---|---|
Assert | 断语办法调集 |
TestCase | 表明一个测验事例 |
TestSuite | 包括一组TestCase,构成一组测验 |
TestResult | 搜集测验成果 |
JUnit的一些注意事项及标准:
(1) 测验办法必须运用@Test 润饰
(2) 测验办法必须运用public void 进行润饰,不能带参数
(3) 测验代码的包应该和被测验代码包结构坚持共同
(4) 测验单元中的每个办法必须能够独立测验,办法间不能有任何依靠
(5) 测验类一般运用 Test作为类名的后缀
(6) 测验办法使一般用test 作为办法名的前缀
JUnit失败成果阐明:
(1) Failure:测验成果和预期成果不共同导致,表明测验不经过
(2) error:由反常代码引起,它能够发生于测验代码本身的错误,也能够是被测代码的Bug
3.2 JUnit内容
(1) 断语的API
断语办法 | 断语描述 |
---|---|
assertNull(String message, Object object) | 检查方针是否为空,不为空报错 |
assertNotNull(String message, Object object) | 检查方针是否不为空,为空报错 |
assertEquals(String message, Object expected, Object actual) | 检查方针值是否持平,不持平报错 |
assertTrue(String message, boolean condition) | 检查条件是否为真,不为真报错 |
assertFalse(String message, boolean condition) | 检查条件是否为假,为真报错 |
assertSame(String message, Object expected, Object actual) | 检查方针引证是否持平,不持平报错 |
assertNotSame(String message, Object unexpected, Object actual) | 检查方针引证是否不等,持平报错 |
assertArrayEquals(String message, Object[] expecteds, Object[] actuals) | 检查数组值是否持平,遍历比较,不持平报错 |
assertArrayEquals(String message, Object[] expecteds, Object[] actuals) | 检查数组值是否持平,遍历比较,不持平报错 |
assertThat(String reason, T actual, Matcher<? super T> matcher) | 检查方针是否满意给定规则,不满意报错 |
(2) JUnit常用注解:
1) @Test: 界说一个测验办法 @Test(excepted=xx.class): xx.class 表明反常类,表明测验的办法抛出此反常时,认为是正常的测验经过的 @Test(timeout = 毫秒数) :测验办法履行时刻是否契合预期。
2) @BeforeClass: 在所有的办法履行前被履行,static 办法大局只会履行一次,并且第一个运转。
3) @AfterClass:在所有的办法履行之后进行履行,static 办法大局只会履行一次,最终一个运转。
4) @Before:在每一个测验办法被运转前履行一次。
5) @After:在每一个测验办法运转后被履行一次。
6) @Ignore:所润饰的测验办法会被测验运转器忽略。
7) @RunWith:能够更改测验履行器运用junit测验履行器。
3.3 JUnit运用
3.3.1 Controller层单元测验
(1) Springboot中运用maven引进Junit十分简单, 运用如下依靠即可引进:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
(2) 上面运用main办法事例能够运用如下的Junit代码完结:
@RunWith(SpringRunner.class)
@SpringBootTest(classes = MainApplication.class)
public class StudentControllerTest {
// 注入Spring容器
@Autowired
private WebApplicationContext applicationContext;
// 模仿Http恳求
private MockMvc mockMvc;
@Before
public void setupMockMvc(){
// 初始化MockMvc方针
mockMvc = MockMvcBuilders.webAppContextSetup(applicationContext).build();
}
/**
* 新增学生测验用例
* @throws Exception
*/
@Test
public void addStudent() throws Exception{
String json="{"name":"张三","className":"三年级一班","age":"20","sex":"男"}";
mockMvc.perform(MockMvcRequestBuilders.post("/student/save") //结构一个post恳求
// 发送端和接纳端数据格式
.contentType(MediaType.APPLICATION_JSON_UTF8)
.accept(MediaType.APPLICATION_JSON_UTF8)
.content(json.getBytes())
)
// 断语校验回来的code编码
.andExpect(MockMvcResultMatchers.status().isOk())
// 增加处理器打印回来成果
.andDo(MockMvcResultHandlers.print());
}
}
只需求在类或许指定办法上右键履行即可,能够直接充当postman作业拜访指定url,且不需求写恳求代码,这些都由东西主动完结。
(3)事例中相关组件介绍
本事例中结构mockMVC方针时,也能够运用如下办法:
@Autowired
private StudentController studentController;
@Before
public void setupMockMvc(){
// 初始化MockMvc方针
mockMvc = MockMvcBuilders.standaloneSetup(studentController).build();
}
其间MockMVC是Spring测验结构供给的用于REST恳求的东西,是对Http恳求的模仿,无需发动整个模块就能够对Controller层进行调用,速度快且不依靠网络环境。
运用MockMVC的根本步骤如下:
-
mockMvc.perform履行恳求
-
MockMvcRequestBuilders.post或get结构恳求
-
MockHttpServletRequestBuilder.param或content增加恳求参数
-
MockMvcRequestBuilders.contentType增加恳求类型
-
MockMvcRequestBuilders.accept增加响应类型
-
ResultActions.andExpect增加成果断语
-
ResultActions.andDo增加回来成果后置处理
-
ResultActions.andReturn履行完结后回来相应成果
3.3.2 Service层单元测验
能够编写如下代码对Service层查询办法进行单测:
@RunWith(SpringRunner.class)
@SpringBootTest
public class StudentServiceTest {
@Autowired
private StudentService studentService;
@Test
public void getOne() throws Exception {
Student stu = studentService.selectByKey(5);
Assert.assertThat(stu.getName(),CoreMatchers.is("张三"));
}
}
履行成果:
3.3.3 Dao层单元测验
能够编写如下代码对Dao层保存办法进行单测:
@RunWith(SpringRunner.class)
@SpringBootTest
public class StudentDaoTest {
@Autowired
private StudentMapper studentMapper;
@Test
@Rollback(value = true)
@Transactional
public void insertOne() throws Exception {
Student student = new Student();
student.setName("李四");
student.setMajor("计算机学院");
student.setAge(25);
student.setSex('男');
int count = studentMapper.insert(student);
Assert.assertEquals(1, count);
}
}
其间@Rollback(value = true) 能够履行单元测验之后回滚所新增的数据,坚持数据库不发生脏数据。
3.3.4 反常测验
(1) 在service层界说一个反常情况:
public void computeScore() {
int a = 10, b = 0;
}
(2) 在service的测验类中界说单元测验办法:
@Test(expected = ArithmeticException.class)
public void computeScoreTest() {
studentService.computeScore();
}
(3) 履行单元测验也会经过,原因是@Test注解中的界说了反常
3.3.5 测验套件测多个类
(1) 新建一个空的单元测验类
(2) 运用注解@RunWith(Suite.class)和@SuiteClasses标明要一同单元测验的类
@RunWith(Suite.class)
@Suite.SuiteClasses({ StudentServiceTest.class, StudentDaoTest.class})
public class AllTest {
}
运转成果:
3.3.6 idea中检查单元测验掩盖率
(1) 单测掩盖率
测验掩盖率是衡量测验进程作业本身的有效性,进步测验功率和削减程序bug,进步产品可靠性与安稳性的指标。
统计单元测验掩盖率的意义:
1) 能够洞察整个代码中的根底组件功能的所有盲点,发现相关问题。
2) 进步代码质量,一般掩盖率低表明代码质量也不会太高,由于单测不经过本来就映射出考虑到各种情况不行充沛。
3) 从掩盖率的达标上能够进步代码的规划能力。
(2) 在idea中检查单元测验掩盖率很简单,只需依照图中示例的图标运转,或许在单元测验办法或类上右键Run ‘xxx’ with Coverage即可。履行成果是一个表格,列出了类、办法、行数、分支掩盖情况。
(3) 在代码中会标识出掩盖情况,绿色的是已掩盖的,红色的是未掩盖的。
(4) 假如想要导出单元测验的掩盖率成果,能够运用如下图所示的办法,勾选 Open generated HTML in browser
导出成果:
3.3.7 JUnit插件主动生成单测代码
(1) 装置插件,重启idea生效
(2) 装备插件
(3) 运用插件
在需求生成单测代码的类上右键generate…,如下图所示。
生成成果:
4. 单元测验东西-Mockito
4.1 Mockito简介
在单元测验进程中建议不要依靠特定的接口与数据来源,此时就触及到对相关数据的模仿,比方Http和JDBC的回来成果等,能够运用虚拟方针即Mock方针进行模仿,使得单元测验不在耦合。
Mock进程的运用条件:
(1) 实践方针时很难被结构出来的
(2) 实践方针的特定行为很难被触发
(3) 实践方针或许当时还不存在,比方依靠的接口还没有开发完结等等。
Mockito官网:https://site.mockito.org 。Mockito和JUnit一样是专门针对Java语言的mock数据结构,它与同类的EasyMock和jMock功能十分类似,但是该东西更加简单易用。
Mockito的特色:
(1) 能够模仿类不仅仅是接口
(2) 经过注解办法简单易懂
(3) 支撑顺序验证
(4) 具有参数匹配器
4.2 Mockito运用
maven引进spring-boot-starter-test会主动将mockito引进到工程中。
4.2.1 运用事例
(1) 在之前的代码中在界说一个BookService接口, 意义是借书接口,暂且不做完成
public interface BookService {
Book orderBook(String name);
}
(2) 在之前的StudentService类中新增一个orderBook办法,意义是学生预订书本办法,其间完成内容调用上述的BookService的orderBook办法。
public Book orderBook(String name) {
return bookService.orderBook(name);
}
(3) 编写单元测验办法,测验StudentService的orderBook办法
@Test
public void orderBookTest() {
Book expectBook = new Book(1L, "钢铁是怎样炼成的", "书架A01");
Mockito.when(bookService.orderBook(any(String.class))).thenReturn(expectBook);
Book book = studentService.orderBook("");
System.out.println(book);
Assert.assertTrue("预订书本不符", expectBook.equals(book));
}
(4) 履行成果:
(5) 成果解析
上述内容并没有完成BookService接口的orderBook(String name)办法。但是运用mockito进行模仿数据之后,却经过了单元测验,原因就在于Mockito替换了本来要在StudentService的orderBook办法中获取的方针,此处就模仿了该方针很难获取或当时无法获取到,用模仿数据进行替代。
4.2.2 相关语法
常用API:
上述事例顶用到了mockito的when、any、theWhen等语法。接下来介绍下都有哪些常用的API:
1)mock:模仿一个需求的方针
2)when:一般配合thenXXX一同运用,表明当履行什么操作之后怎样。
3)any: 回来一个特定方针的缺省值,上例中标识能够填写任何String类型的数据。
4)theReturn: 在履行特定操作后回来指定成果。
5)spy:发明一个监控方针。
6)verify:验证特定的行为。
7)doReturn:回来成果。
8)doThrow:抛出特定反常。
9)doAnswer:做一个自界说响应。
10)times:操作履行次数。
11)atLeastOnce:操作至少要履行一次。
12)atLeast:操作至少履行指定的次数。
13)atMost:操作至多履行指定的次数。
14)atMostOnce:操作至多履行一次。
15)doNothing:不做任何的处理。
16)doReturn:回来一个成果。
17)doThrow:抛出一个指定反常。
18)doAnswer:指定一个特定操作。
19)doCallRealMethod:用于监控方针回来一个实在成果。
4.2.3 运用要点
(1) 打桩
Mockito中有Stub,所谓存根或许叫打桩的概念,上面事例中的Mockito.when(bookService.orderBook(any(String.class))).thenReturn(expectBook);便是打桩的意义,先界说好假如依照既定的办法调用了什么,成果就输出什么。然后在运用Book book = studentService.orderBook(“”); 即依照指定存根输出指定成果。
@Test
public void verifyTest() {
List mockedList = mock(List.class);
mockedList.add("one");
verify(mockedList).add("one"); // 验证经过,由于前面界说了这个桩
verify(mockedList).add("two"); // 验证失败,由于前面没有界说了这个桩
}
(2) 参数匹配
上例StudentService的orderBook办法中的any(String.class) 即为参数匹配器,能够匹配任何此处界说的String类型的数据。
(3) 次数验证
@Test
public void timesTest() {
List mockedList = mock(List.class);
when(mockedList.get(anyInt())).thenReturn(1000);
System.out.println(mockedList.get(1));
System.out.println(mockedList.get(1));
System.out.println(mockedList.get(1));
System.out.println(mockedList.get(2));
// 验证经过:get(1)被调用3次
verify(mockedList, times(3)).get(1);
// 验证经过:get(1)至少被调用1次
verify(mockedList, atLeastOnce()).get(1);
// 验证经过:get(1)至少被调用3次
verify(mockedList, atLeast(3)).get(1);
}
(4) 顺序验证
@Test
public void orderBookTest1() {
String json = "{"id":12,"location":"书架A12","name":"三国演义"}";
String json1 = "{"id":21,"location":"书架A21","name":"水浒传"}";
String json2 = "{"id":22,"location":"书架A22","name":"红楼梦"}";
String json3 = "{"id":23,"location":"书架A23","name":"西游记"}";
when(bookService.orderBook("")).thenReturn(JSON.parseObject(json, Book.class));
Book book = bookService.orderBook("");
Assert.assertTrue("预订书本有误", "三国演义".equals(book.getName()));
when(bookService.orderBook("")).thenReturn(JSON.parseObject(json1, Book.class)).
thenReturn(JSON.parseObject(json2, Book.class)).
thenReturn(JSON.parseObject(json3, Book.class));
Book book1 = bookService.orderBook("");
Book book2 = bookService.orderBook("");
Book book3 = bookService.orderBook("");
Book book4 = bookService.orderBook("");
Book book5 = bookService.orderBook("");
// 悉数验证经过,按顺序最终打桩打了3次,大于3次依照最终方针输出
Assert.assertTrue("预订书本有误", "水浒传".equals(book1.getName()));
Assert.assertTrue("预订书本有误", "红楼梦".equals(book2.getName()));
Assert.assertTrue("预订书本有误", "西游记".equals(book3.getName()));
Assert.assertTrue("预订书本有误", "西游记".equals(book4.getName()));
Assert.assertTrue("预订书本有误", "西游记".equals(book5.getName()));
}
(5) 反常验证
@Test(expected = RuntimeException.class)
public void exceptionTest() {
List mockedList = mock(List.class);
doThrow(new RuntimeException()).when(mockedList).add(1);
// 验证经过
mockedList.add(1);
}