Java程序员如何写单元测试

段落引用很多公司都要求程序员写单元测试,并要求单元测试的代码覆盖率达到80%到90%.但是很多公司的程序员都是被需求和bug赶着跑的,自然没有精力去写单元测试.但是单元测试对于很多比较成熟的项目来说是有必要的,毕竟要用上十几年,甚至几十年.但是对于那种代码生命周期在短短几个月的功能,比如为了某个节日某个活动写的临时程序能跑通即可.

一.什么代码需要写单元测试

  1. 复杂了业务逻辑即Service需要写单元测试
  2. 复杂的算法
  3. 调用比较多的工具类
  4. dao层

二. 如何进行单元测试的编写

  1. 我们测试的是这个类方法的逻辑,不是对应这个类依赖的底层逻辑,即我们测试的不是整个模块 即如果,我们要对 UserServiceImpl 这个类写 UserServiceImplTest 单元测试类, UserServiceImplTest 要测的是 UserServiceImpl这个类本身的逻辑

  2. 基于此我们, 所以我们要把 UserServiceImpl类依赖的子模块都mock掉,来测本身的逻辑.

  3. 单元测试是做白盒测试, 所以我们写测试用例的时候需要做代码覆盖,当某个方法,会根据某些参数,或者依赖模块的返回数据走不同的逻辑时,我们要写多个testcase 方法.

  4. 针对数据访问层的操作,可以使用两种方案,一个是mock jdbc ,把数据库或者redis操作变成内存操作, 另外一种方案是, 在Test 的 before , after 的方法中做对于数据的初始化,和数据的清理

三. mock 数据比较好用的框架

    compile 'org.powermock:powermock-module-junit4:2.0.7'
    compile 'org.powermock:powermock-api-mockito2:2.0.7'
    compile 'org.mockito:mockito-core:3.3.3'

mockito 是个比较好的mockito工具,他可以mockito类和接口,你只需要管你需要调用的方法返回值即可. 但是它不支持 静态方法,和final方法,所以我们一般把 powermock 和 mockito 搭配着用

下面是一个单元测试的例子

package run.halo.app.service.impl;

import com.qiniu.common.Zone;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.mockito.junit.MockitoJUnitRunner;
import run.halo.app.cache.AbstractStringCacheStore;
import run.halo.app.model.entity.Option;
import run.halo.app.model.properties.QiniuOssProperties;
import run.halo.app.repository.OptionRepository;
import run.halo.app.service.OptionService;

import java.util.HashMap;
import java.util.Map;
import java.util.Optional;

import static org.hamcrest.Matchers.equalTo;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertThat;
import static org.mockito.BDDMockito.given;
import static org.mockito.BDDMockito.then;

/**
 * OptionService test.
 *
 * @author johnniang
 * @date 3/22/19
 */
@RunWith(MockitoJUnitRunner.class)
public class OptionServiceImplTest {

    // ## 这个服务类依赖的底层逻辑
    @Mock
    private OptionRepository optionRepository;

    // ## 这个服务类依赖的底层逻辑
    @Mock
    private AbstractStringCacheStore cacheStore;

    //## 被测试的服务类
    @InjectMocks
    private OptionServiceImpl optionService;

    @Test
    public void getQiniuAutoZoneTest() {
        getQiniuZoneTest("", Zone.autoZone());
    }

    @Test
    public void getQiniuAutoZoneOfNullOptionTest() {
        getQiniuZoneTest(Zone.autoZone(), null);
    }

    @Test
    public void getQiniuZ0ZoneTest() {
        getQiniuZoneTest("z0", Zone.zone0());
    }

    @Test
    public void getQiniuZ1ZoneTest() {
        getQiniuZoneTest("z1", Zone.zone1());
    }

    @Test
    public void getQiniuZ2ZoneTest() {
        getQiniuZoneTest("z2", Zone.zone2());
    }

    @Test
    public void getQiniuAs0ZoneTest() {
        getQiniuZoneTest("as0", Zone.zoneAs0());
    }

    @Test
    public void getQiniuNa0ZoneTest() {
        getQiniuZoneTest("na0", Zone.zoneNa0());
    }

    private void getQiniuZoneTest(String region, Zone actualZone) {
        getQiniuZoneTest(actualZone, new Option("", region));
    }

    private void getQiniuZoneTest(Zone actualZone, Option option) {
        QiniuOssProperties zoneProperty = QiniuOssProperties.OSS_ZONE;

        
 // 未你这个测试方法需要调用方法定义mock的返回值 ,当然某个方法你不需要调用即不用定义
     given(optionRepository.findByKey(zoneProperty.getValue())).willReturn(Optional.ofNullable(option));


        Map<String, Object> optionMap = new HashMap<>(1);
        optionMap.put(zoneProperty.getValue(), Optional.ofNullable(option).map(Option::getValue).orElse(null));

        
 // 未你这个测试方法需要调用方法定义mock的返回值 ,当然某个方法你不需要调用即不用定义
        given(cacheStore.getAny(OptionService.OPTIONS_KEY, Map.class)).willReturn(Optional.of(optionMap));

        // When
        Zone zone = optionService.getQnYunZone();

        // Then
        then(cacheStore).should().getAny(OptionService.OPTIONS_KEY, Map.class);

        assertNotNull(zone);
        assertThat(zone.getRegion(), equalTo(actualZone.getRegion()));
    }
}