元测试

一、什么是单元测试

维基百科是这样描述的: 在计算机编程中,单元测试又称为模块测试,是针对程序模块来进行正确性检验的测试工作。程序单元是应用的最小可测试部件。在过程化编程中,一个单元就是单个程序、函数、过程等;对于面向对象编程,最小单元就是方法,包括基类、抽象类、或者派生类中的方法。

单元测试的理念其实⼀直是编程的⼀部分。我们第⼀次编写计算机程序时,肯定会输⼊⼀些样本数据,查看其是否按照你的期望执⾏。如果结果不符合预期,你肯定在代码⾥穿插过⼤量的System.out.println,确保每个原⼦节点都符合预期。这个过程其实就是把复杂问题拆解成原⼦化的问题、逐⼀攻破的过程。单元测试的⽬的也⼀样,是保障软件程序中每个最⼩单位的正确性,从⽽保障由最⼩单位构建起来的复杂系统的正确性。

二、单元测试的意义

2.1. 验证代码逻辑

对于一个代码模块,编写单元测试的过程,就是对代码逻辑进行重新 Review 的过程;而执行单元测试的过程,就是验证代码是否按既定逻辑运行的过程。

2.2 减少代码缺陷

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

2.3. 促进代码设计

在编写单测的过程中,如果发现单测代码非常难写,一般表明被测试的代码包含了太多的依赖或职责,需要反思代码的合理性,进而促进代码设计的优化。

2.4. 便于缺陷定位

由于单元规模较小,复杂性较低,因而发现错误后容易隔离和定位,有利于代码调试工作。

2.5.  增强代码信心

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

三、测试分类

  • 单元测试:针对的是软件设计的最小单元--程序模块(面向过程中是函数、过程;面向对象中是类。)

  • 集成测试:针对的是通过了单元测试的各个模块所集成起来的组件进行检验 (各个模块集成后所实现的功能. )

    开发同学自己编写的,对外部环境(数据库、文件系统、外部系统、消息队列等)有真实调用的测试就是集成测试。

  • 系统测试:针对的是集成好的软件系统,作为整个计算机系统的一个元素,与计算机硬件\外设\某些支持软件\数据和人员等其他系统元素结合在一起,要在实际的运行环境中,对计算机系统进行一系列的集成测试和确认测试.

3.1 各种测试对比

单元测试 集成测试 系统级别测试
编写人员 开发 开发 开发 / 测试
编写场地 生产代码仓库内 生产代码仓库内 生产代码仓库内 /  生产代码仓库外
编写时间 代码发布前 代码发布前 代码发布前 /  代码发布后
编写成本
编写难度
反馈速度 极快,秒级 较慢,分钟级 慢,天级别
覆盖面积 代码行覆盖60-80%  分支覆盖40-60% 功能级别覆盖HappyPath 核心保障链路
环境依赖 代码级别,不依赖环境 依赖日常或本地环境 依赖预发或生产环境
外部依赖模拟 全部模拟 部分模拟 不模拟,完全使用真实环境

四、Java 单元测试基础知识

4.1 前言

单元测试的理念其实⼀直是编程的⼀部分。我们第⼀次编写计算机程序时,肯定会输⼊⼀些样本数据,查看其是否按照你的期望执⾏。如果结果不符合预期,你肯定在代码⾥穿插过⼤量的System.out.println,确保每个原⼦节点都符合预期。这个过程其实就是把复杂问题拆解成原⼦化的问题、逐⼀攻破的过程。单元测试的⽬的也⼀样,是保障软件程序中每个最⼩单位的正确性,从⽽保障由最⼩单位构建起来的复杂系统的正确性。

4.2 Java 单元测试技巧之 PowerMock

4.2.1 准备环境

PowerMock 是一个扩展了其它如 EasyMock 等 mock 框架的、功能更加强大的框架。PowerMock 使用一个自定义类加载器和字节码操作来模拟静态方法、构造方法、final 类和方法、私有方法、去除静态初始化器等等。

引入 PowerMock 包 为了引入 PowerMock 包,需要在 pom.xml 文件中加入下列 maven 依赖:

4.2.2 引入 PowerMock 包
<dependency>
    <groupId>org.powermock</groupId>
    <artifactId>powermock-module-junit4</artifactId>
    <version>2.0.9</version>
    <scope>test</scope>
</dependency>
<dependency>  
    <groupId>junit</groupId>  
    <artifactId>junit</artifactId>  
    <version>4.13.1</version>  
    <scope>test</scope>
</dependency>
<dependency>  
    <groupId>org.mockito</groupId>  
    <artifactId>mockito-core</artifactId>  
    <version>3.3.3</version>  
    <scope>test</scope>
</dependency>
    <dependency>  
        <groupId>org.powermock</groupId>  
        <artifactId>powermock-api-mockito2</artifactId>  
        <version>2.0.9</version>  
        <scope>test</scope>
    </dependency>
4.3.3 集成 SpringBoot 项目

在 SpringBoot 项目中,需要在 pom.xml 文件中加入 JUnit 的 maven 依赖:

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
        </dependency>

单元测试案例

接口

@Controller
public class DbController {

    @Resource
    private DbService dbService;

    @GetMapping("/db")
    public String getResult(String request) {
       return dbService.getDate(request);
    }
}

业务代码

@Service
public class DbServiceImpl implements DbService {
    @Override
    public String getDate(String request) {

        return "hello world";
    }
}
@RunWith(MockitoJUnitRunner.class)
class SpringTestApplicationTests {

    @Mock
    private DbService dbService;

    @Mock
    DbMapper  dbMapper;

    @InjectMocks
    DbController     demoController;


    @Test
    public void testGetDate_success_getFrom() {

        MockitoAnnotations.openMocks(this);
        // Arrange - 定义 mock 行为
        Mockito.doReturn("ExpectedResults").when(dbService).getDate("Request");
        // Act - 调用被测试方法
        String result = demoController.getResult("Request");

        Mockito.verify(dbService).getDate("Request");
        Mockito.verifyNoMoreInteractions(dbService);

        // Assert - 验证结果
        Assert.assertEquals("ExpectedResults", result);

    }
}
4.3.5 mock 语句
public static <T> T mock(Class<T> classToMock)

用途:可以用于模拟指定类的对象实例。

4.3.6  spy 语句
public static <T> T spy(T... reified)

如果一个对象,我们只希望模拟它的部分方法,而希望其它方法跟原来一样,可以使用 PowerMockito.spy 方法代替 PowerMockito.mock 方法。于是,通过 when 语句设置过的方法,调用的是模拟方法;而没有通过 when 语句设置的方法,调用的是原有方法。

4.3.7 when 语句

用途:用于模拟对象方法,先执行原始方法,再返回期望的值、异常、应答,或调用真实的方法。

返回期望值
      when(dbService.getDate("Request")).thenReturn("hello world");
返回期望异常
  when(dbService.getDate("some arg"))
                .thenThrow(new RuntimeException())
                .thenReturn("foo");
返回期望应答
  when(dbService.getDate("Request")).thenAnswer(invocation -> "ExpectedResults");
调用真是方法
when(dbService.getDate("Request")).thenCallRealMethod();
doReturn().when()模式

用用于模拟对象方法,直接返回期望的值、异常、应答,或调用真实的方法,无需执行原始方法。

返回期望值
 Mockito.doReturn("ExpectedResults").when(dbService).getDate("Request");

返回期望异常

 Mockito.doThrow(new RuntimeException()).when(dbService).getDate("some arg");

返回期望应答

  Mockito.doAnswer(invocation -> "foo").when(dbService).getDate("some arg");

模拟无返回值

 Mockito.doNothing().when(dbService).getDate("some arg");

调用真实方法

Mockito.doCallRealMethod().when(dbService).getDate("some arg");
两种模式的主要区别

两种模式都用于模拟对象方法,在 mock 实例下使用时,基本上是没有差别的。但是,在 spy 实 例 下使用时, when().thenReturn() 模 式 会 执 行 原 方法,而doReturn().when()模式不会执行原方法。

4.3.8 参数匹配器

在执行单元测试时,有时候并不关心传入的参数的值,可以使用参数匹配器。

参数匹配器(any)

Mockito 提 供 Mockito.anyInt() 、 Mockito.anyString 、Mockito.any(Classclazz)等来表示任意值。

参数匹配器(eq)

当我们使用参数匹配器时,所有参数都应使用匹配器。 如果要为某一参数指定特定值时,就需要使用 Mockito.eq()方法。

Logo

技术共进,成长同行——讯飞AI开发者社区

更多推荐