Skip to content

单元测试的一些原则及实践

Posted on:2023年12月17日 at 03:11
  |   8 min read   |  

单测是什么

单元测试是自动验证软件的最小可测试部分(例如函数或方法)在隔离状态下按预期工作的过程。它是软件开发中的基础实践,允许开发人员及早识别和修复缺陷,降低更改成本并增强可维护性。

单元测试对开发人员的一些关键好处:

为了实现高效的单元测试,开发人员可以使用各种测试框架,例如JUnit、Jest 和 pytest。这些框架促进了测试实现并加快了执行速度。高质量的单元测试应该是自动化的、全面的、独立的、可重复的和易于编写的。通过将这些测试集成到持续集成/持续部署(CI/CD)流水线中,可以确保代码在每次提交时都保持稳定性和质量。

因为上面的这些好处,我们在开发软件时,写单元测试是很有必要的。

怎么写单测

那应该怎么写呢? 写单侧应该遵循下面的一些原则,遵循它们可以让你的单测写得有效,且高效。

下面,我会使用 Java + Maven + Junit 5(Jupiter)来做一些示范。

单测的例子

以下例子都是基于 Mockito 、Junit 5(Jupiter),请注意版本差异

多个数据

package com.stark.utils;

import org.assertj.core.util.Arrays;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.CsvSource;

import static org.junit.jupiter.api.Assertions.assertEquals;

class SequenceUtilTest {

    @ParameterizedTest
    @CsvSource(value = {
            "foo|bar|bar2|tar,foo,1",
            "foo|bar|bar2|tar,bar,2",
            "foo|bar|foo|tar,foo,1"
    })
    void skippedHeadsUtil_strings(String sequence, String contains, int expected) {
        // given
        String[] array = Arrays.array(sequence.split("\\|"));
        // when
        int skipped = SequenceUtil.skippedHeadsUntil(array, item -> item.equals(contains));
        // then
        assertEquals(expected, skipped);
    }
}

MockitoExtension 而不是 SpringRunner

使用 @Spy@Mock@InjectMock

package com.stark.biz.action.helpAction;

import com.alibaba.fastjson.JSON;
import com.stark.pojo.request.GitProjectVo;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.Spy;
import org.mockito.junit.jupiter.MockitoExtension;
import org.springframework.http.HttpMethod;
import org.springframework.http.ResponseEntity;
import com.stark.utils.ElasticsearchUtil;
import org.junit.jupiter.api.extension.ExtendWith;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.Arguments;
import org.junit.jupiter.params.provider.MethodSource;
import org.mockito.InjectMocks;

import java.util.Collections;
import java.util.List;

import static org.junit.jupiter.api.Assertions.*;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.*;

@ExtendWith(MockitoExtension.class)
class GitLabHelperTest {

    private static GitProjectVo respVo;
    @Spy
    private GitLabHelper vcsProvider = new GitLabHelper("http://127.0.0.1:8080", "1");
    @InjectMocks
    private JenkinsPluginLogProcessorFactory factory;
    @Mock
    private ElasticsearchUtil elasticsearchUtil;

    @BeforeEach
    void setUp() {
        reset(vcsProvider);
        respVo = new GitProjectVo();
        respVo.setId(1L);
        respVo.setBranch("master");
    }
    @Test
    void searchProjectInfoById() throws Exception {
        // given
        String respStr = JSON.toJSONString(respVo);

        doReturn(ResponseEntity.ok(respStr)).when(vcsProvider).gitHttpRequest(any(), any(), any());

        // when
        GitProjectVo actual = vcsProvider.searchProjectInfoById(1L);

        // then
        verify(vcsProvider).gitHttpRequest("/api/v4/projects/1", null, HttpMethod.GET);
        assertEquals(respVo, actual);
    }

    public static Stream<Arguments> mapping() {
        return Stream.of(
                Arguments.of(JenkinsPluginLogProcessorFactory.SCAN, JenkinsPluginLogProcessor4Scan.class),
                Arguments.of(JenkinsPluginLogProcessorFactory.TEST, JenkinsPluginLogProcessor4Test.class)
        );
    }

    @ParameterizedTest
    @MethodSource("mapping")
    void provide(String type, Class<? extends JenkinsPluginLogProcessor> clazz) {
        JenkinsPluginLogProcessor processor = factory.provide(type, null);
        assertEquals(clazz, processor.getClass());
    }
}

最快,最直接的方法是在本地使用 maven 命令验证单测没问题,再提交代码。

覆盖率报告位置

Jacoco 的报告位置在 target/site/jacoco/index.html ,执行下面的命令之后可以直接打开查看报告详情

一般项目

mvn org.jacoco:jacoco-maven-plugin:prepare-agent test prepare-package org.jacoco:jacoco-maven-plugin:report

如果是多模块项目,可以考虑加上 -pl moduleA,moduleB 来指定只运行某几个模块的单测以及单测覆盖率报告生成

使用了 PowerMock

如果是多模块项目,同样也可以考虑加上 -pl moduleA,moduleB 来指定只运行某几个模块的单测以及单测覆盖率报告生成

Linux:

mkdir -p $HOME/.jacoco \
&& export JACOCO_VERSION=0.8.11 \
&& mvn clean dependency:copy -Dartifact="org.jacoco:org.jacoco.agent:${JACOCO_VERSION}:jar:runtime" -DoutputDirectory=$HOME/.jacoco  test-compile org.jacoco:jacoco-maven-plugin:${JACOCO_VERSION}:instrument  surefire:test org.jacoco:jacoco-maven-plugin:${JACOCO_VERSION}:restore-instrumented-classes org.jacoco:jacoco-maven-plugin:${JACOCO_VERSION}:report  -Djacoco-agent.destfile=./target/jacoco.exec -Dmaven.test.additionalClasspath="$HOME/.jacoco/org.jacoco.agent-${JACOCO_VERSION}-runtime.jar"

如果是在 Windows cmd 里,则执行:

mkdir %USERPROFILE%\.jacoco 2>nul

set JACOCO_VERSION=0.8.11

mvn clean dependency:copy -Dartifact="org.jacoco:org.jacoco.agent:%JACOCO_VERSION%:jar:runtime" -DoutputDirectory=%USERPROFILE%\.jacoco test-compile org.jacoco:jacoco-maven-plugin:%JACOCO_VERSION%:instrument  surefire:test org.jacoco:jacoco-maven-plugin:%JACOCO_VERSION%:restore-instrumented-classes org.jacoco:jacoco-maven-plugin:%JACOCO_VERSION%:report -Djacoco-agent.destfile=./target/jacoco.exec -Dmaven.test.additionalClasspath="%USERPROFILE%\.jacoco\org.jacoco.agent-%JACOCO_VERSION%-runtime.jar"

学习推荐