作者:京东物流 秦彪

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,且不需求写恳求代码,这些都由东西主动完结。

Java单元测试浅析(JUnit+Mockito)

(3)事例中相关组件介绍

本事例中结构mockMVC方针时,也能够运用如下办法:

@Autowired
private StudentController studentController;
@Before
public void setupMockMvc(){
   // 初始化MockMvc方针
   mockMvc = MockMvcBuilders.standaloneSetup(studentController).build();
}

其间MockMVC是Spring测验结构供给的用于REST恳求的东西,是对Http恳求的模仿,无需发动整个模块就能够对Controller层进行调用,速度快且不依靠网络环境。

运用MockMVC的根本步骤如下:

  1. mockMvc.perform履行恳求

  2. MockMvcRequestBuilders.post或get结构恳求

  3. MockHttpServletRequestBuilder.param或content增加恳求参数

  4. MockMvcRequestBuilders.contentType增加恳求类型

  5. MockMvcRequestBuilders.accept增加响应类型

  6. ResultActions.andExpect增加成果断语

  7. ResultActions.andDo增加回来成果后置处理

  8. 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("张三"));
    }
}

履行成果:

Java单元测试浅析(JUnit+Mockito)

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);
    }
}

Java单元测试浅析(JUnit+Mockito)

其间@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注解中的界说了反常

Java单元测试浅析(JUnit+Mockito)

3.3.5 测验套件测多个类

(1) 新建一个空的单元测验类

(2) 运用注解@RunWith(Suite.class)和@SuiteClasses标明要一同单元测验的类

@RunWith(Suite.class)
@Suite.SuiteClasses({ StudentServiceTest.class, StudentDaoTest.class})
public class AllTest {
}

运转成果:

Java单元测试浅析(JUnit+Mockito)

3.3.6 idea中检查单元测验掩盖率

(1) 单测掩盖率

测验掩盖率是衡量测验进程作业本身的有效性,进步测验功率和削减程序bug,进步产品可靠性与安稳性的指标。

统计单元测验掩盖率的意义:

1) 能够洞察整个代码中的根底组件功能的所有盲点,发现相关问题。

2) 进步代码质量,一般掩盖率低表明代码质量也不会太高,由于单测不经过本来就映射出考虑到各种情况不行充沛。

3) 从掩盖率的达标上能够进步代码的规划能力。

(2) 在idea中检查单元测验掩盖率很简单,只需依照图中示例的图标运转,或许在单元测验办法或类上右键Run ‘xxx’ with Coverage即可。履行成果是一个表格,列出了类、办法、行数、分支掩盖情况。

Java单元测试浅析(JUnit+Mockito)

(3) 在代码中会标识出掩盖情况,绿色的是已掩盖的,红色的是未掩盖的。

Java单元测试浅析(JUnit+Mockito)

(4) 假如想要导出单元测验的掩盖率成果,能够运用如下图所示的办法,勾选 Open generated HTML in browser

Java单元测试浅析(JUnit+Mockito)

导出成果:

Java单元测试浅析(JUnit+Mockito)

3.3.7 JUnit插件主动生成单测代码

(1) 装置插件,重启idea生效

Java单元测试浅析(JUnit+Mockito)

(2) 装备插件

Java单元测试浅析(JUnit+Mockito)

Java单元测试浅析(JUnit+Mockito)

(3) 运用插件

在需求生成单测代码的类上右键generate…,如下图所示。

Java单元测试浅析(JUnit+Mockito)

生成成果:

Java单元测试浅析(JUnit+Mockito)

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) 履行成果:

Java单元测试浅析(JUnit+Mockito)

(5) 成果解析

上述内容并没有完成BookService接口的orderBook(String name)办法。但是运用mockito进行模仿数据之后,却经过了单元测验,原因就在于Mockito替换了本来要在StudentService的orderBook办法中获取的方针,此处就模仿了该方针很难获取或当时无法获取到,用模仿数据进行替代。

Java单元测试浅析(JUnit+Mockito)

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);
    }