单元测试最佳实践

1 为什么需要单元测试

(1)执行时间短

功能测试,一些场景需要串联很多步骤,测试人员对这些步骤不一定每一步都熟悉。

单元测试,相比功能测试实践更短,不一定需要对整个系统了解。

(2)方便回归

使用单元测试可以很方便的进行回归测试。

(3)可执行的文档

当编写具有良好命名的测试用例时,每个用例可以清晰的说明对于给定的输入会有怎样的输出。此外,测试用例还应可以验证方法是否能够正常工作。

(4)便于对代码解耦

当代码耦合性很高的时候,一般很难进行单元测试。因此,能反向促进代码解耦优化。

2 良好的单元测试特征

(1)快速

对于大型成熟项目可能会有数千个测试用例。每个测试用例应尽可能快的运行,最好在毫秒级别。

(2)隔离

单元测试是独立的,可以单独运行而不依赖外部元素,如文件系统或数据库。

(3)可重复

在不改变输入的情况下,单元测试的输出结果应保持不变。

(4)自检查

单元测试应自动检测测试是否通过而无需人工干预。

(5)耗时少

如果测试代码所花费的时间远超编写代码的时间,应当考虑重构代码以便于更好测试。确保编写测试所花费的。

3 代码覆盖率监测工具JaCoCo

  1. 行覆盖率:度量被测程序的每行代码是否被执行,判断标准行中是否至少有一个指令被执行。
  2. 类覆盖率:度量计算class类文件是否被执行。
  3. 分支覆盖率:度量if和switch语句的分支覆盖情况,计算一个方法里面的总分支数,确定执行和不执行的分支数量。
  4. 方法覆盖率:度量被测程序的方法执行情况,是否执行取决于方法中是否有至少一个指令被执行。
  5. 指令覆盖:计数单元是单个java二进制代码指令,指令覆盖率提供了代码是否被执行的信息,度量完全 独立源码格式。
    圈复杂度:在(线性)组合中,计算在一个方法里面所有可能路径的最小数目,缺失的复杂度同样表示测试案例没有完全覆盖到这个模块。

4 单元测试编写-springboottest

(1)生成单元测试类


选择要测试的serviceImpl,右键生成测试类,选择junit5

在这里插入图片描述

 生成的测试类如下:

class UserPayServiceImplTest {

    @Test
    void pay() {
    }
}

(2)加入测试

MyframeApplication对应项目的启动类。如果不写这个的话,下面的service就无法自动注入。
测试类启动时会先启动项目,再执行测试方法。也就是说测试类的测试依赖项目启动环境。

@SpringBootTest(
        /*MyframeApplication对应项目的启动类。如果不写这个的话,下面的service就无法自动注入。
         测试类启动时会先启动项目,再执行测试方法。也就是说测试类的测试依赖项目启动环境。*/
        classes = MyframeApplication.class,  
        /*因为测试的是service,禁用web以加快项目启动速度*/
        webEnvironment = WebEnvironment.NONE
)
class UserPayServiceImplTest {

    @Autowired
    private UserPayService userPayService; //自动注入要测试的service
    @Test
    void pay() {
        userPayService.pay(new BigDecimal(300));// 测试接口方法
    }
}

(3)执行测试

在这里插入图片描述

 成功通过测试类进入了实际代码的断点。

(4)应用MOCK

1. 基本方法

在创建该类的单元测试类前,先讲一下这几个基本注解:

@Test:使用该注解标注的public void方法会表示为一个测试方法;
@BeforeClass:表示在类中的任意public static void方法执行之前执行;
@AfterClass:表示在类中的任意public static void方法之后执行;
@Before:表示在任意使用@Test注解标注的public void方法执行之前执行;
@After:表示在任意使用@Test注解标注的public void方法执行之后执行;

2. Mock的概念

所谓的mock就是创建一个类的虚假的对象,在测试环境中,用来替换掉真实的对象,以达到两大目的:

  1. 验证这个对象的某些方法的调用情况,调用了多少次,参数是什么等等
  2. 指定这个对象的某些方法的行为,返回特定的值,或者是执行特定的动作

使用Mock之前,需要在@Before或@BeforeClass对应的方法中添加如下,表示添加mock注解初始化。

MockitoAnnotations.initMocks(this);

另外需要补充以下几个常用的测试注解:

  • @InjectMocks:通过创建一个实例,它可以调用真实代码的方法,其余用@Mock(或@Spy)注解创建的mock将被注入到用该实例中。

  • @Mock:对函数的调用均执行mock(即虚假函数),不执行真正部分。

  • @Spy:对函数的调用均执行真正部分。

Mockito中的Mock和Spy都可用于拦截那些尚未实现或不期望被真实调用的对象和方法,并为其设置自定义行为。二者的区别在于Mock不真实调用,Spy会真实调用。

3 mock案例

  • 单个类的mock

(1)被测试类

class ExampleService {
  
    int add(int a, int b) {
        return a+b;
    }
  
}

(2)测试类

import org.junit.Assert;
import org.junit.Test;
import static org.mockito.Mockito.*;

public class MockitoDemo {
    @Spy
    private ExampleService spyExampleService;
  
    @Mock
    private ExampleService mockExampleService;

    // 测试 spy
    @Test
    public void test_spy() {
      
        // 默认会走真实方法
        Assert.assertEquals(3, spyExampleService.add(1, 2));

        // 打桩后,不会走了
        when(spyExampleService.add(1, 2)).thenReturn(10);
        Assert.assertEquals(10, spyExampleService.add(1, 2));

        // 但是参数不匹配的调用,依然走真实方法
        Assert.assertEquals(3, spyExampleService.add(2, 1));
      
    }

    // 测试 mock
    @Test
    public void test_mock() {

        // 默认返回结果是返回类型int的默认值
        Assert.assertEquals(0, mockExampleService.add(1, 2));

    }
}
  • 内部调用其他方法的mock

(1)被测试类

class MockExampleService {
    
    int add(int a, int b) {
        return a+b;
    }
  
}

class InjectMockExampleService {
    
    @Autowired
    private MockExampleService mockExampleService;
      
    int add(int a, int b) {
        return mockExampleService.add(a, b);
    }
  
}

(2)测试类

import org.junit.Assert;
import org.junit.Test;
import static org.mockito.Mockito.*;

public class MockitoDemo {
    @InjectMock
    private InjectMockExampleService injectMockExampleService;
  
    @Mock
    private MockExampleService mockExampleService;
    
    // 初始化汉顺
    @Before
    public void init() throw Exception {
            MockitoAnnotations.initMocks(this);
    }

    // 使用Mockito模拟
    @Test
    public void test() {
      
        // 模拟MockExampleServiceadd函数对于任何参数返回都为10
                when(mockExampleService.add(anyInt(), anyInt())).thenReturn(10);
        // InjectMock会走真实的add方法,只不过mock会返回一个模拟的结果
        Assert.assertEquals(10, injectMockExampleService.add(1, 2));

    }
}

常用的 Mockito 方法

Mockito的使用,一般有以下几种组合:参考链接

  • do/when:包括doThrow(…).when(…)/doReturn(…).when(…)/doAnswer(…).when(…)
  • given/will:包括given(…).willReturn(…)/given(…).willAnswer(…)
  • when/then: 包括when(…).thenReturn(…)/when(…).thenAnswer(…)/when(…).thenThrow(…)

Mockito 有多种匹配函数,部分如下:

函数名匹配类型
any()所有对象类型
anyInt()基本类型 int、非 null 的 Integer 类型
anyChar()基本类型 char、非 null 的 Character 类型
anyShort()基本类型 short、非 null 的 Short 类型
anyBoolean()基本类型 boolean、非 null 的 Boolean 类型
anyDouble()基本类型 double、非 null 的 Double 类型
anyFloat()基本类型 float、非 null 的 Float 类型
anyLong()基本类型 long、非 null 的 Long 类型
anyByte()基本类型 byte、非 null 的 Byte 类型
anyString()String 类型(不能是 null)
anyList()List<T> 类型(不能是 null)
anyMap()Map<K, V>类型(不能是 null)

参考文献地址:

net单元测试

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值