# 单元测试实战（四种覆盖详解、测试实例）

## 前言

（这里需要注意的就是单测的预期结果 一定要针对需求/设计逻辑去写，而不是针对实现去写，否则单测将毫无意义，照着错误的实现设计出的case也很可能是错的）

## 覆盖类型

### 1、行覆盖 Statement Coverage

```public Integer fun3(Integer a, Integer b, Integer x) {

if (a > 1 && b == 0) {
x = x + a;
}
if (a == 2 || x > 1) {
x += 1;
}
return x;
}
```

 a b x 预期结果 TC1 2 0 3 6
```@Test
public void testFun3StatementCoverage(){
Integer res = demoService.fun3(2,0,3);
Assert.assertEquals(6,res.intValue());
}
```

```public Integer fun4(Integer a, Integer b, Integer x) {

if (a > 1 || b == 0) {
x += a;
}
if (a == 2 || x > 1) {
x += 1;
}
return x;
}
```

### 2、判定覆盖 / 分支覆盖 (Decision Coverage/Branch Coverage)

```public Integer fun3(Integer a, Integer b, Integer x) {

if (a > 1 && b == 0) {
x = x + a;
}
if (a == 2 || x > 1) {
x += 1;
}
return x;
}
```

 a b x 预期结果 TC2 2 0 1 4 TC3 3 1 1 1
```@Test
public void testFun3DecisionCoverage(){
Integer res = demoService.fun3(2,0,1);
Assert.assertEquals(4,res.intValue());
res = demoService.fun3(3,1,1);
Assert.assertEquals(1,res.intValue());
}
```

tc2 时， A，B均为true；tc3时，A，B均为false。

### 3、条件覆盖 Condition Coverage

```public Integer fun3(Integer a, Integer b, Integer x) {

if (a > 1 && b == 0) {
x = x + a;
}
if (a == 2 || x > 1) {
x += 1;
}
return x;
}
```

 a b x 预期结果 TC4 2 0 3 6 TC5 0 1 0 0
```@Test
public void testFun3ConditionCoverage(){
Integer res = demoService.fun3(2,0,3);
Assert.assertEquals(6,res.intValue());
res = demoService.fun3(0,1,0);
Assert.assertEquals(0,res.intValue());
}

```

### 4、路径覆盖 Path Coverage

```public Integer fun3(Integer a, Integer b, Integer x) {

if (a > 1 && b == 0) {
x = x + a;
}
if (a == 2 || x > 1) {
x += 1;
}
return x;
}
```

 a b x 预期结果 经过路径 TC6 0 1 0 0 1 TC7 3 0 -3 0 2 TC8 2 1 3 4 3 TC9 2 0 3 6 4
```@Test
public void testFun3PathCoverage(){
Integer res = demoService.fun3(0,1,0);
Assert.assertEquals(0,res.intValue());

res = demoService.fun3(3,0,-3);
Assert.assertEquals(0,res.intValue());

res = demoService.fun3(2,1,3);
Assert.assertEquals(4,res.intValue());

res = demoService.fun3(2,0,3);
Assert.assertEquals(6,res.intValue());

}
```

## 实战部分（Mock）

### mock是什么

Mock这个单词的意思就是假的，模拟的。mock就是用一些特殊的代码，模拟一下外部依赖，将我们的测试代码与外部依赖解耦。

Spock：比较新的测试框架，基于groovy。优点:语法优雅清晰，缺点:groovy需要学习，没有经历时间的考验。

Mockito：最常用，最可靠的测试框架，优点就是没有什么太大的缺点。

PowerMock：为了解决Mockito等基于cglib的框架无法mock 私有、静态方法而产生的框架。

### 简单Mock实战

service是最需要关注的地方，重要的逻辑一般都在这里；同时又有诸多的外部依赖，用这一层做实际mock的实例是最合适的。

```/**
* @author zhangpeng34
* Created on 2019/1/18 下午9:15
**/
@Service
public class UserServiceImpl implements UserService {
@Autowired
UserDao userDao;

@Override
public User findUserById(Long id) {
return userDao.findById(id).orElse(null);
}

@Override
if(StringUtils.isEmpty(name)){
return null;
}
User user = new User();
user.setName(name);
return userDao.save(user);
}
}

```

```/**
*
* 单元测试，测试的目的是对java代码逻辑进行测试。
* 单纯的逻辑测试，不应该加载外部依赖，所有的外部依赖应该mock掉，只关注本身逻辑。
* 例如，需要测试service层时，所依赖的dao等，应提前mock掉，设置好测试需要的输入和输出即可。
* dao层的逻辑应由dao层的测试保证，service层默认dao层是正确的。
**/
@RunWith(MockitoJUnitRunner.class)
public class UserServiceTests {
//mock注解创建一个被mock的实例
@Mock
UserDao userDao;

//InjectMocks代表创建一个实例，其他带mock注解的示例将被注入到该实例用。
//可以用该注解创建要被测试的实例，将实例所需的依赖用mock注解创建，即可mock掉依赖
@InjectMocks
UserServiceImpl UserServiceImpl;

/**
* 初始化时设置一些需要mock的方法和返回值
* 这里的设置表示碰到userDao的save方法，且参数为任一User类的实例时，返回提前预设的值
*/
@Before
public void init(){
User user =new User();
user.setId(1L);
Mockito.when(userDao.save(any(User.class)))
.thenReturn(user);
}
//正向流程
@Test
Assert.assertEquals(1L,user.getId().longValue());
}
//异常分支，name为null
@Test
Assert.assertEquals(null,user);
}
//将各个分支都写出test和assert
//............
}
```

### 集成测试

Controller层是较为适合集成测试的地方，这里用Controller层来做集成测试的示例。

```@RestController
public class UserController {

@Autowired
UserService userService;

@PostMapping(value = "/user")
public Object register(String name) {
}

@GetMapping(value = "/user/{userId}")
public Object getUserInfo(@PathVariable Long userId) {
User user = userService.findUserById(userId);
if (user != null) {
return user;
}
return "fail";

}

public Object login(Long id, String pwd) {
User user =  userService.findUserById(id);
if(user!=null){
return user;
}
return "fail";
}
}

```

```spring.profiles=it
server.port=9898

spring.h2.console.enabled=true
spring.h2.console.path=/h2
spring.datasource.driver-class-name=org.h2.Driver
spring.datasource.url=jdbc:h2:mem:db;DB_CLOSE_DELAY=-1

spring.datasource.max-wait=10000
spring.datasource.max-active=5
spring.datasource.test-on-borrow=true
spring.datasource.test-while-idle = true
spring.datasource.validation-query = SELECT 1

# jpa
spring.jpa.hibernate.ddl-auto=update
#spring.jpa.hibernate.ddl-auto= create-drop
spring.jpa.hibernate.naming_strategy=org.hibernate.cfg.ImprovedNamingStrategy
spring.jpa.show-sql=true
spring.jpa.generate-ddl=true
```

```@RunWith(SpringRunner.class)
@SpringBootTest
@ActiveProfiles("it")
@AutoConfigureMockMvc
public class UserControllerTests {
@Autowired
private MockMvc mvc;

@Autowired
UserDao userDao;

Long userId;

@Before
public void init(){
User user = new User();
user.setName("111");
userId = userDao.save(user).getId();
System.out.println(userId);
}

/**
* 测试/user/{userId}
* @throws Exception
*/
@Test
public void testGetUser() throws Exception {
//success
this.mvc.perform(get("/user/"+userId)).andExpect(status().isOk())
.andExpect(content().json("{ \"id\": "+userId+", \"name\": \"111\" }"));
//fail
this.mvc.perform(get("/user/"+(userId+100))).andExpect(status().isOk())
.andExpect(content().string("fail"));
}

/**
* @throws Exception
*/
@Test
public void exampleTest2() throws Exception {
//success
.andExpect(status().isOk())
.andExpect(content().json("{ \"id\": "+userId+", \"name\": \"111\" }"));

//fail
.andExpect(status().isOk())
.andExpect(content().string("fail"));
}
}
```

`@ActiveProfiles("it")` 这个注解就是制定本测试代码加载的配置文件，”it“指 文件名 `application-XX.properties` 中间的xx，spring会自动根据名称去加载对应的配置文件。

`init()` 方法就是在内存数据库中构造自己需要的数据，这是集成测试最常见的步骤。