[TOC]

## 概述

### 什么是单元测试

**单元测试**(Unit Test) 是针对 **程序的最小单元** 来进行正确性检验的测试工作。程序单元是应用的最小可测试部件。一个单元可能是**单个程序、类、对象、方法**等。

如何区分单元测试和集成测试，一般情况下，单元测试应该不依赖数据库，网络，I/O和其他模块，否则就是集成测试

### 单元测试特性

- 单元性（快速）
  - 测试力度足够小，能够精确定位问题
  - 单一职责：一个测试`case`只负责一条路径，测试代码中不允许有复杂的逻辑条件
- 独立性（无依赖）
  - 避免单元测试之间的依赖关系，一个测试的运行不依赖于其他测试代码的运行结果
  - 不依赖数据：与数据库交互时不能假设数据存在，可调用`Dao`中的`Insert`方法来准备数据
  - 不依赖外部环境，建议使用`Mock`
- 可重复性（幂等性）
  - 不破坏数据：和数据库相关的单元测试，必须设定自动回滚机制`@Transactional`
  - 每次执行的结果都相同
- 自验证
  - 不靠人来检查，必须使用断言
  - 尽可能断言具体的内容（简单的为空判断起不到太大的作用）
  - 测试代码必须有好的前置条件和后置断言
  - 异常的验证使用`expectedExceptions`

### 单元测试价值

编写单元测试代码并不是一件容易的事情，那为什么还需要去花费时间和精力来编写单元测试呢？

1. **减少bug**  我们的工程都是分层分模块的，每个模块都是独立的逻辑部分。通过单元测试保障工程各个“零件”按“规格”（需求）执行，就能保证整个“机器”（项目）运行正确，最大限度减少bug。

2. **改进设计**  越是良好设计的代码，一般越容易编写单元测试，多个小的方法的单测一般比大方法（成百上千行代码）的单测代码要简单、要稳定，一个依赖接口的类一般比依赖具体实现的类容易测试，所以在编写单测的过程中，如果发现单测代码非常难写，一般表明被测试的代码包含了太多的依赖或职责，需要反思代码的合理性，进而推进代码设计的优化，形成正向循环。

3. **便于项目交接或多人协作**

4. **减少调试及错误修复时间** 如果程序有bug，我们运行一次单元测试，就能帮助找到逻辑错误的代码，及时修复问题。 统计表明定位并修复一个BUG所需的代价，正比于目标代码规模的平方。

5. **放心重构**  如今持续型的项目越来越多，代码不断的在变化和重构，通过单元测试，开发可以放心的修改重构代码，减少改代码时的心理负担，提高重构的成功率。

6. **增强对自己代码的信心** 写完代码，单元测试通过，虽说单元测试并不能百分之百保证代码完全正确运行，但起码大部分测过的逻辑都是可用的，这会增强我们的信心，也会增加工作成就感。

![](https://raw.githubusercontent.com/fengyuchenglun/forever-image/master/img/20190129092559.png)

## 测试框架选型

-  基础/核心
  - Junit
  - TestNG
-  验证
  - AssertJ
  - Harmcrest
-  Mock
  - Mockito
  - Easymock
  - Jmockit
  - powermock
-  Spring
  - Spring Test
  - unitils-spring
-  数据库
  - DbUnit
  - 内存数据库(H2)



### junit VS testng

| 特性                   |               JUnit4                |          TestNG           |
| :--------------------- | :---------------------------------: | :-----------------------: |
| 测试注解               |                @Test                |           @Test           |
| 在测试套件执行之前执行 |                  –                  |       @BeforeSuite        |
| 在测试套件执行之后执行 |                  –                  |        @AfterSuite        |
| 在测试之前执行         |                  –                  |        @BeforeTest        |
| 在测试之后执行         |                  –                  |        @AfterTest         |
| 在测试组执行之前执行   |                  –                  |       @BeforeGroups       |
| 在测试组执行之后执行   |                  –                  |       @AfterGroups        |
| 在测试类执行之前执行   |            @BeforeClass             |       @BeforeClass        |
| 在测试类执行之后执行   |             @AfterClass             |        @AfterClass        |
| 在测试方法执行之前执行 |               @Before               |       @BeforeMethod       |
| 在测试方法执行之后执行 |               @After                |       @AfterMethod        |
| 忽略测试               |               @ignore               |    @Test(enbale=false)    |
| 预期异常               | @Test(expected=expectedExceptions） | @Test(expectedExceptions) |
| 超时                   |           @Test(timeout)            |      @Test(timeout)       |

 通过上面的对比，**使用TestNG作为Java项目的单元测试框架**

### Mock工具

常用的mock工具包括：jmock 、EasyMock、 mockito、PowerMock、jmockit

1. jmock :只能mock接口、mock代码复杂不简洁、需要显示指出mock对象
2. EasyMock：不能对static,final,private进行mock,可读相对简洁但不如mockito
3. mockito：mockito 支持接口和方法，不能对static,final,private进行mock、mock 代码简单简洁 api简洁 可读性高、注解来表示mock对象
4. PowerMock：EasyMock和mockito上扩充，能对static,final,private进行mock（测试架构设计良好的代码，一般并不需要这些功能 ），能覆盖测试抛异常
5. jmockit：类似PowerMock，但代码可读性不强

mock工具最重要的是可用可读性和简洁还有api能力，所以最终**选择了mockito，如需要对 static, final, private方法进行mock，可用PowerMock扩展**。

### 断言工具

验证框架：AssertJ,Harmcrest，junit/testng自带assert.

1. assertJ:流式断言，代码即用例，直观易懂;方便定制的断言器 ;引入了java8依赖的lamda特性 
2. Harmcrest：提供了大量丰富的匹配器，能够让断言可读性更高，断言样板代码量更小，更易维护。为Junit4断言的一部分；
3. junit/testng自带assert:unit/testng自带；可读性差

因此**选择assertJ作为断言库**

### 数据库工具

暂无，涉及数据库的测试暂时定义为集成测试，通过端到端的集成测试完成

### 选型总结

- 基础单元测试框架:testng

- Mock:mockito＋PowerMock扩展(如需要对static, final, private方法进行mock，可用PowerMock扩展)

- 断言:AssertJ


## 单元测试规范

关于单元测试有几点需要说明：

- 单元测试不仅仅是为了保证代码没有bug，也是提升代码质量（重构和设计）的手段，并对持续集成提供支持
- 单元测试并不是越多越好，功能简单的，非关键模块的代码不强制要求编写单元测试，但编写也无妨
- 单元测试的编写要求可读，可维护，可信赖
- 单元测试是面向单一功能点，与数据库，网络，文件I/O，以及外部模块均无关，可快速完成的测试，善用mockito进行隔离（Controller层与Service层）
- 与数据库交互的测试（DAO层），定义为集成测试，可通过Postman工具完成从Controller层 -> Service层 -> DAO层的端到端测试，目前不要求编写单元测试案例，人工完成
- 单元测试应该充分考虑边界和异常，核心业务模块的代码保证尽量高的代码测试覆盖率
- 单元测试对持续集成友好，可实现自动化测试

**强烈建议通读resources\《有效的单元测试》了解如何编写可读，可维护，可信赖的单元测试，避免典型反范式出现**

### 基本准则

1. 【强制】核心应用核心业务增量代码一定要写单元测试。
2. 【强制】单元测试类的命名应该和被测试类保持一致xxxTest，测试方法为被测方法名xxx，所在包的命名和被测试类的包名保持一致。注意：单元测试持续集成的时候会用到maven插件maven-surefire-plugin，这个插件目标会自动执行测试源码路径（默认为src/test/Java/）下所有符合命名模式的测试类。模式为：**/*Test.java：任何子目录下所有命名以Test结尾的Java类。**
3. 【强制】单元测试应该是全自动/非交互式的，测试套件通常是定期执行的，执行过程必须完全自动化才有意义。输出结果需要人工检查的测试不是一个好的单元测试。单元测试中不准使用System.out来进行人肉验证，必须使用assert来验证、代码执行路径方式验证（mock）或者数据存在性验证（dao数据库操作）。
4. 【强制】保持测试的独立性，为了保证测试稳定可靠且便于维护，测试用例之间决不能有相互依赖，也不能依赖执行的先后次序。
5. 【强制】开发必须保证自己写的单元测试能在本地执行通过，才能提交。
6. 【强制】重视边界值测试。
7. 【强制】不需要的单元测试直接删除，不要注释掉，如果非要注释请写清楚注释理由。
8. 【推荐】对于单元测试，要保证测试力度足够小，能够精确定位问题。对于依赖本应用之外的所有第三方环境的单元测试，统一使用Mock的方式进行测试，即做到尽可能的摆脱对环境依赖、持续重复运行。
9. 【推荐】写测试代码的目的是为了提高业务代码质量，严禁为达到测试要求而书写不规范测试代码；对于不可测的代码建议做必要的重构，使代码变的可测

### 目录&命名规范

目录

- `src/test/java`
- `src/test/resources`

命名

- 包的命名和被测试类的包名保持一致

- 测试类名和被测试类保持一致`XxxTest`

- 测试方法

  - `1:1`情形: `testXxx`
  - `1:N`情形: `testXxxx_测式场景`

  ```java
  /**
   * DemoService测试类(XxxTest).
   */
  public class DemoServiceTest {
  
      /**
       * 必要的注释
       */
      public void testFindById_Success(Long Id) {
      }
         /**
       * 必要的注释
       */
      public void testFindById_EmptyId(Long Id) {
      }
        /**
       * 必要的注释
       */
      public void testFindById_NotExist(Long Id) {
      }
  }
  ```



### 驱动方式

- TDD：在开发功能代码之前，先编写单元测试用例代码，测试代码驱动产品代码开发

- 边开发边单测：每个功能点，先写产品代码，再写单测代码

- 先开发再单测：先完成所有或部分功能点，在提测前补单测

**TDD做好比较难，存在争议也比较多；建议采用边开发边单测的方式。**

### 执行单元测试

- 本地在编写代码的时候直接可以运行单元测试得到反馈。

- 在maven中也实现了运行单元测试的功能，在生成二进制包之前可以对代码进行回归测试，构建工具可以通过命令行调用，这是自动化构建的前提。

- 依托于构建工具提供的自动化特性，在持续集成、持续部署的过程中可以执行自动化构建，在自动化构建的过程中通过构建工具执行单元测试，这是持续集成的流程中的重要步骤。

### 测试范围

理想：覆盖所有类、函数、逻辑

实际：至少保证一些重要的类和逻辑应该测试到

- 测试重点
  - 公共库、底层库、核心库

    - 公共、底层如`dao`层代码以及关键的业务逻辑`servie`、`controller`层代码

  - 重要复杂的算法、逻辑、功能以及容易出错的分支

    - 接口较为复杂，含有较多的逻辑分支时，比如订单状态变化

  - 系统测试无法验证或者系统测试成本高的功能点  

    - 对于涉及到业务逻辑的异常，需要覆盖

  - `service`层
    - 单元测试应尽可能测试到所有的业务逻辑
    - 无法做到测试所有业务逻辑则至少应该测试业务的主要逻辑
    - 有外部依赖时使用mock

  - `controller`层
    - 输入输出参数的控制与处理
    - 注意边界和异常值
    - 使用mockMvc
    - 有外部依赖时使用mock

|              | 重要程度 | 推荐框架                    | 说明                                                         |
| ------------ | -------- | --------------------------- | :----------------------------------------------------------- |
| service层    | 高       | testng + mokito             | 各种中间件和hsf以及dao都mock掉. 重点在于逻辑和场景覆盖       |
| controller层 | 中       | testng + SpringTest/MockMvc | controller层建议只做校验入参和组装出参. 服务下沉到service.springMVC的还好测试. |

### 评价指标(待讨论)

- 通过率
  - 新项目代码通过率100%
  - 旧代码逐步改善
- 覆盖率
  - 代码行覆盖率达到40%
  - 增量代码的覆盖率要达到60%以上



## Service层单元测试

**主要是通过Mockito来模拟dao层接口调用。主要验证业务逻辑的正确性；**具体可参考工程demo.

```java
/**
 * 角色服务实现类
 *
 * @author duanledexianxianxian
 * @data 2018 /11/30
 */
@Service
public class RoleServiceImpl extends ServiceImpl<RoleMapper, RoleEntity> implements IRoleService {

    @Transactional(rollbackFor = Exception.class)
    @Override
    public RoleVO addRole(RoleEntity role) {
        // 判断角色编码是否已经存在
        Wrapper wrapper = new QueryWrapper<RoleEntity>().lambda().eq(RoleEntity::getCode, role.getCode());
        Integer cnt = this.baseMapper.selectCount(wrapper);
        if (cnt > 0) {
            throw new BizException(UserErrorConstants.USER_ROLE_0001);
        }
        this.baseMapper.insert(role);
        return ForeverBeanUtil.copyObject(role, RoleVO.class);
    }
}
```

```java
/**
 * 角色服务类单元测试接口
 * AbstractTestNGSpringContextTests 注入spring上下文
 *
 * @author duanledexianxianxian
 * @data 2019 /1/29
 */
public class RoleServiceImplTest {
    /**
     * 创建一个Mock。创建一个Mock。原方法被mock掉。
     */
    @Mock
    RoleMapper roleMapper;
    /**
     * 创建一个实例，其余用@Mock（或@Spy）注解创建的mock将被注入到用该实例中。
     */
    @InjectMocks
    RoleServiceImpl roleService;

    /**
     * Sets up.
     */
    @BeforeMethod
    public void setUp() {
        // 将标记了 @Mock 或 @Spy 的属性注入到 service 中。
        MockitoAnnotations.initMocks(this);
    }

    /**
     * 添加角色
     * 正常情况
     */
    @Test
    public void testAddRole_Success() {
        // service入参
        RoleEntity param = new RoleEntity();
        param.setCode("role_code");
        param.setName("role_name");
        param.setRemark("remark");
        // 模型调用
        // 用户不存在
        when(roleMapper.selectCount(any(Wrapper.class))).thenReturn(0);
        // 插入记录
        when(roleMapper.insert(param)).thenAnswer((Answer<Integer>) invocation -> {
            param.setId(1L);
            return 1;
        });
        // 调用结果
        RoleVO result = roleService.addRole(param);
        // 验证结果
        assertThat(result).isNotNull();
        assertThat(result.getId()).isEqualTo(1L);
        verify(roleMapper, atLeastOnce()).selectCount(any(Wrapper.class));
        verify(roleMapper, atLeastOnce()).insert(param);
    }


    /**
     * 添加角色
     * 用户已经存在
     * 抛出异常
     */
    @Test(expectedExceptions = BizException.class, expectedExceptionsMessageRegExp = "USER_ROLE_0001")
    public void testAddRole_Exist() {
        // service入参
        RoleEntity param = new RoleEntity();
        param.setCode("role_code");
        param.setName("role_name");
        param.setRemark("remark");
        // 模型调用
        when(roleMapper.selectCount(any(Wrapper.class))).thenReturn(1);
        when(roleMapper.insert(param)).thenAnswer((Answer<Integer>) invocation -> {
            param.setId(1L);
            return 1;
        });
        // 调用结果
        RoleVO result = roleService.addRole(param);
    }
}
```

#### What mock?

Mock就是做一个假的object，对这个object里的方法的调用，都会被Mockito拦截，然后返回用户预设的行为。这样可以绕过需要从其它地方拿数据的地方，直接返回用户预设的数据，进行单元测试。

#### When mock?

1. 方法里有多个方法，已经测试过的方法 就可以mock掉

2. 其它的协同模块尚未开发完成

3. 被测试模块需要和一些不容易构造、比较复杂的对象进行交互

4. 由于不能肯定其它模块的正确性，我们也无法确定测试中发现的问题是由哪个模块引起的

5. 外部资源，比如文件系统、数据源。如果被测对象对此类外部资源依赖性非常强，而其行为的不可预测性很可能导致测试的随机失败，此类的外部资源也适合进行

6. 网络交互，外部系统调用接口，如果两个被测模块之间是通过网络进行交互的

7. UI：因为UI很多时候都是用户行为触发事件，系统本身只是对这些触发事件进行相应，对这类UI做Mock


#### mockito常用方法

1. 需要mock某个类时，在属性上标注注解@InjectMocks或@Mock或@spy，分别用法如下

     1. @Mock相当于mockito帮助简单的实例化bean，因此无法执行原生方法。适用于整个bean都mock,如DAO。
     2. @spy被它代理的bean, 默认执行原生方法。定制返回值的会调用mock实例。 
  2. @Resource自动注入mock管理的bean. 解决二层和多层bean嵌套的部分mock场景。即injectProductBusiness->customerBusiness->feeBusiness 时，如果想mock feeBusiness中的某个方法. 
  3. 一般使用Mockito的步骤:
     1. 模拟任何外部依赖并将这些模拟对象插入测试代码中
     2. 执行测试中的代码
     3. 验证代码是否按照预期执行
  4. 同时可以结合spring一起管理bean.对bean的管理应该是spring先进行一系列的如初始化bean操作， 然后mockito会引用spring生成的bean,并对bean里的指定field进行重新注入。以达到实现部分mock功能
5. 常用api
```java
  // 设置mock对象的返回预期值,当调用userDao.getUserById接口时，返回构造好的user对象
    when(userDao.getUserById(userId)).thenReturn(user);
  //mock无返回值的方法   
    doNothing().when(userDao).delete(userId);
  //mock方法抛异常，两种写法
    doThrow(new RuntimeException("测试")).when(userDao).delete(userId);
    when(userDao.getUserById(userId)).thenThrow(new Exception());
  //mock参数类型, anyInt()、anyBoolean()、any(XXX.class)、anyString()、
	matches(".*你好$")、argThat() 自定义的匹配器，见org.mockito.Matchers类
    when(userDao.getUserById(anyLong())).thenReturn(user);//此接口传入任何long型参数都mock
  //mock不同次调用不同返回结果,第一调用返回user1,第二次调用返回user2
    when(userDao.getUserById(anyLong())).thenReturn(user1).thenReturn(user2);
  //超时，在指定的毫秒中执行完毕
    verify(mock,times(2).description("This will print on failure")).get(0);
```

## Controllor层单元测试

**主要是接口测试验证，使用mockito模拟service层**。具体可参考工程demo.

@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT, classes = SpringWebApplication.class)：给单元测试用例提供spring web上下文.

@TestExecutionListeners(listeners = MockitoTestExecutionListener.class): @MockBean注解的类才能被mockito模拟

TestRestTemplate：构建rest请求，发送请求(也可使用MockMvc构建、发送请求以及结果断言);

JsonPath:解析json类库。

```java
@Controller
@RequestMapping("/api/v1/roles")
@Slf4j
public class RoleApi {
    /**
     * The Role service.
     */
    @Autowired
    private IRoleService roleService;

    /**
     * 增加角色
     *
     * @param domain the domain
     * @return the role vo
     */
    @PostMapping("")
    public RoleVO addRole(@Validated @RequestBody RoleDomain domain) {
        log.info("-------------------输入参数:{}------------------", domain);
        RoleEntity entity = ForeverBeanUtil.copyObject(domain, RoleEntity.class);
        return roleService.addRole(entity);
    }
}
```

```java
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT, classes = SpringWebApplication.class)
// {@link TestExecutionListener} to trigger {@link MockitoAnnotations#initMocks(Object)}
// * when {@link MockBean @MockBean} annotations are used.
@TestExecutionListeners(listeners = MockitoTestExecutionListener.class)
public class RoleApiTestRestTemplateTest extends AbstractTestNGSpringContextTests {
    @Autowired
    TestRestTemplate restTemplate;
    @MockBean
    IRoleService roleService;

    // 参数验证
    @DataProvider(name = "roleData")
    public Object[][] createData() {
        return new Object[][]{
                {new RoleDomain(null, null, RandomStringUtils.random(5), null), "SYSTEM_ILL_ARGS"},
                {new RoleDomain(null, RandomStringUtils.random(5), null, null), "SYSTEM_ILL_ARGS"},
                {new RoleDomain(null, RandomStringUtils.random(101), RandomStringUtils.random(5), null), "SYSTEM_ILL_ARGS"},
                {new RoleDomain(null, RandomStringUtils.random(5), RandomStringUtils.random(101), null), "SYSTEM_ILL_ARGS"},
                {new RoleDomain(null, RandomStringUtils.random(5), RandomStringUtils.random(5), RandomStringUtils.random(201)), "SYSTEM_ILL_ARGS"},
        };
    }

    @Test(dataProvider = "roleData")
    public void testAddRole_Invalid(RoleDomain domain, String errorCode) throws Exception {
        when(roleService.addRole(any())).thenReturn(null);
        // 如果需要验证请求头使用postForEntity
        ResponseEntity<ResultData> actual = this.restTemplate.postForEntity("/api/v1/roles", domain, ResultData.class);
        assertThat(actual.getStatusCode()).isEqualTo(HttpStatus.OK);
        assertThat(actual.getBody()).isNotNull();
        assertThat(actual.getBody().getCode()).isEqualTo(errorCode);
        //assertThat(actual.getBody().getCode()).isEqualTo("SUCCESS");
        //assertThat(actual.getBody().getMessage()).isNull();
        //assertThat(actual.getBody().getTraceId()).isNotNull();
        //assertThat(JSONObject.toJSONString(JsonPath.read(actual, "$.data.id"))).isEqualTo("1");
        verify(roleService, never()).addRole(any());
        System.out.println(actual.getBody());
    }

    //正常流程
    @Test
    public void testAddRole_Success() throws Exception {
        RoleDomain domain = new RoleDomain();
        domain.setCode("code");
        domain.setName("name");
        domain.setRemark("remark");
        RoleVO vo = ForeverBeanUtil.copyObject(domain, RoleVO.class);
        vo.setId(1L);
        when(roleService.addRole(any())).thenReturn(vo);
        // 如果需要验证请求头使用postForEntity
        String actual = this.restTemplate.postForObject("/api/v1/roles", domain, String.class);
        //assertThat(actual.getStatusCode()).isEqualTo(HttpStatus.OK);
        //assertThat(actual.getBody()).isNotNull();
        //assertThat(actual.getBody().getCode()).isEqualTo("SUCCESS");
        //assertThat(actual.getBody().getMessage()).isNull();
        //assertThat(actual.getBody().getTraceId()).isNotNull();
        assertThat(JSONObject.toJSONString(JsonPath.read(actual, "$.data.id"))).isEqualTo("1");
    }
}
```

使用IDEA开发工具，建议安装TestMe插件。IDEA自带junit插件，可以生成Junit4单元测试代码；使用TestMe插件可以一键生成testng+mockito模板代码。


## 参考链接

1. http://joel-costigliola.github.io/assertj/
2. http://dbunit.sourceforge.net/
3. https://site.mockito.org/
4. https://testng.org/doc/index.html