查看原文
其他

为什么程序员一定要写单元测试?

鱼皮 程序员鱼皮 2024-01-21

大家好,我是鱼皮,很多初学编程的同学都会认为 “程序员的工作只有开发新功能,功能做完了就完事儿”。但其实不然,保证程序的正常运行、提高程序的稳定性和质量也是程序员的核心工作。

之前给大家分享过企业项目的完整开发流程,其中有一个关键步骤叫 “单元测试”,这篇文章就来聊聊程序员如何编写单元测试吧。

什么是单元测试?

单元测试(Unit Testing,简称 UT)是软件测试的一种,通常由开发者编写测试代码并运行。相比于其他的测试类型(比如系统测试、验收测试),它关注的是软件的 最小 可测试单元。

什么意思呢?

假如我们要实现用户注册功能,可能包含很多个子步骤,比如:

  1. 校验用户输入是否合法
  2. 校验用户是否已注册
  3. 向数据库中添加新用户

其中,每个子步骤可能都是一个小方法。如果我们要保证用户注册功能的正确可用,那么就不能只测试注册成功的情况,而是要尽量将每个子步骤都覆盖到,分别针对每个小方法做测试。比如输入各种不同的账号密码组合来验证 “校验用户输入是否合法” 这一步骤在成功和失败时的表现是否符合预期。

同理,如果我们要开发一个很复杂的系统,可能包含很多小功能,每个小功能都是一个单独的类,我们也需要针对每个类编写单元测试。因为只有保证每个小功能都是正确的,整个复杂的系统才能正确运行。

单元测试的几个核心要点是:

  1. 最小化测试范围:单元测试通常只测试代码的一个非常小的部分,以确保测试的简单和准确。
  2. 自动化:单元测试应该是自动化的,开发人员可以随时运行它们来验证代码的正确性,特别是在修改代码后。而不是每次都需要人工去检查。
  3. 快速执行:每个单元测试的执行时间不能过长,应该尽量做到轻量、有利于频繁执行。
  4. 独立性:每个单元测试应该独立于其他测试,不依赖于外部系统或状态,以确保测试的可靠性和可重复性。

为什么需要单元测试?

通过编写和运行单元测试,开发者能够快速验证代码的各个部分是否按照预期工作,有利于保证系统功能的正确可用,这是单元测试的核心作用。

此外,单元测试还有很多好处,比如:

1)改进代码:编写单元测试的过程中,开发者能够再次审视业务流程和功能的实现,更容易发现一些代码上的问题。比如将复杂的模块进一步拆解为可测试的单元。

2)利于重构:如果已经编写了一套可自动执行的单元测试代码,那么每次修改代码或重构后,只需要再自动执行一遍单元测试,就知道修改是否正确了,能够大幅提高效率和项目稳定性。

3)文档沉淀:编写详细的单元测试本身也可以作为一种文档,说明代码的预期行为。

鱼皮以自己的一个实际开发工作来举例单元测试的重要性。我曾经编写过一个 SQL 语法解析模块,需要将 10000 多条链式调用的语法转换成标准的 SQL 语句。但由于细节很多,每次改进算法后,我都不能保证转换 100% 正确,总会人工发现那么几个错误。所以我编写了一个单元测试来自动验证解析是否正确,每次改完代码后执行一次,就知道解析是否完全成功了。大幅提高效率。

所以无论是后端还是前端程序员,都建议把编写单元测试当做一种习惯,真的能够有效提升自己的编码质量。

如何编写单元测试?

以 Java 开发为例,我们来学习如何编写单元测试。

Java 开发中,最流行的单元测试框架当属 JUnit 了,它提供了一系列的类和方法,可以帮助我们快速检验代码的行为。

1、引入 JUnit

首先我们要在项目中引入 JUnit,演示 2 种方式:

Maven 项目引入

在 pom.xml 文件中引入 JUnit 4 的依赖:

<dependency>
    <groupId>junit</groupId>
    <artifactId>junit</artifactId>
    <version>4.13.2</version>
    <scope>test</scope>
</dependency>

Spring Boot 项目引入

如果在 Spring Boot 中使用 JUnit 单元测试,直接引入 spring-boot-starter-test 包即可:

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

然后会自动引入 JUnit Jupiter,它是 JUnit 5(新版本)的一部分,提供了全新的编写和执行单元测试的方式,更灵活易用。不过学习成本极低,会用 JUnit 4,基本就会用 JUnit Jupiter。

2、编写单元测试

编写一个单元测试通常包括三个步骤:准备测试数据、执行要测试的代码、验证结果。

一般来说,每个类对应一个单元测试类,每个方法对应一个单元测试方法。

编写 JUnit 单元测试

比如我们要测试一个计算器的求和功能,示例代码如下:

import org.junit.Test;
import org.junit.Assert;

public class CalculatorTest {

    // 通过 Test 注解标识测试方法
    @Test
    public void testAdd() {
        // 准备测试数据
        long a = 2;
        long b = 3;
        
        // 执行要测试的代码
        Calculator calculator = new Calculator();
        int result = calculator.add(23);
        
        // 验证结果
        Assert.assertEquals(5, result);
    }
}

上述代码中的 Assert 类是关键,提供了很多断言方法,比如 assertEquals(是否相等)、assertNull(是否为空)等,用来对比程序实际输出的值和我们预期的值是否一致。

如果结果正确,会看到如下输出:

如果结果错误,输出如下,能够清晰地看到执行结果的差异:

Spring Boot 项目单测

如果是 Spring Boot 项目,我们经常需要对 Mapper 和 Service Bean 进行测试,则需要使用 @SpringBootTest 注解来标识单元测试类,以开启对依赖注入的支持。

以测试用户注册功能为例,示例代码如下:

import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.Test;
import org.springframework.boot.test.context.SpringBootTest;

import javax.annotation.Resource;

@SpringBootTest
public class UserServiceTest {

    @Resource
    private UserService userService;

    @Test
    void userRegister() {
        // 准备数据
        String userAccount = "yupi";
        String userPassword = "";
        String checkPassword = "123456";
        // 执行测试
        long result = userService.userRegister(userAccount, userPassword, checkPassword);
        // 验证结果
        Assertions.assertEquals(-1, result);
        // 再准备一组数据,重复测试流程
        userAccount = "yu";
        result = userService.userRegister(userAccount, userPassword, checkPassword);
        Assertions.assertEquals(-1, result);
    }
}

3、生成测试报告

如果系统的单元测试数量非常多(比如 1000 个),那么只验证某个单元测试用例是否正确、查看单个结果是不够的,我们需要一份全面完整的单元测试报告,便于查看单元测试覆盖度、评估测试效果和定位问题。

测试覆盖度 是衡量测试过程中被测试到的代码量的一个指标,一般情况下越高越好。测试覆盖度 100% 表示整个系统中所有的方法和关键语句都被测试到了。

下面推荐 2 种生成单元测试报告的方法。

使用 IDEA 生成单测报告

直接在 IDEA 开发工具中选择 Run xxx with Coverage 执行单元测试类:

然后就能看到测试覆盖度报告了,如下图:

显然 Main 方法没有被测试到,所以显示 0%。

除了在开发工具中查看测试报告外,还可以导出报告为 HTML 文档:

导出后,会得到一个 HTML 静态文件目录,打开 index.html 就能在浏览器中查看更详细的单元测试报告了:

这种方式简单灵活,不用安装任何插件,比较推荐大家日常学习使用。

使用 jacoco 生成单测报告

JaCoCo 是一个常用的 Java 代码覆盖度工具,能够自动根据单元测试执行结果生成详细的单测报告。

它的用法也很简单,推荐按照官方文档中的步骤使用。

官方文档指路:https://www.eclemma.org/jacoco/trunk/doc/maven.html

首先在 Maven 的 pom.xml 文件中引入:

<plugin>
  <groupId>org.jacoco</groupId>
  <artifactId>jacoco-maven-plugin</artifactId>
  <version>0.8.11</version>
</plugin>

当然,只引入 JaCoCo 插件还是不够的,我们通常希望在执行单元测试后生成报告,所以还要增加 executions 执行配置,示例代码如下:

<plugin>
    <groupId>org.jacoco</groupId>
    <artifactId>jacoco-maven-plugin</artifactId>
    <version>0.8.11</version>
    <configuration>
        <includes>
            <include>com/**/*</include>
        </includes>
    </configuration>
    <executions>
        <execution>
            <id>pre-test</id>
            <goals>
                <goal>prepare-agent</goal>
            </goals>
        </execution>
        <execution>
            <id>post-test</id>
            <phase>test</phase>
            <goals>
                <goal>report</goal>
            </goals>
        </execution>
    </executions>
</plugin>

然后执行 Maven 的 test 命令进行单元测试:

测试结束后,就能够在 target 目录中,看到生成的 JaCoCo 单元测试报告网站了:

打开网站的 index.html 文件,就能看到具体的测试报告结果,非常清晰:

通常这种方式会更适用于企业中配置流水线来自动化生成测试报告的场景。

实践

编程导航星球的用户中心项目详细讲解了如何使用 JUnit 编写规范的单元测试。

👉🏻 编程导航原创项目教程系列:https://yuyuanweb.feishu.cn/wiki/SePYwTc9tipQiCktw7Uc7kujnCd

👇🏻 点击下方阅读原文,获取鱼皮往期编程干货

往期推荐

斩获 2 个 Offer,开心!

我用过很多代码生成器,还是选了他

创业真的太累了。。

公司来了位 “新同事”,专门提醒员工接水?

大一结束,我经历了觉醒与顿悟!

我敢打赌,这个架构你一定知道!

继续滑动看下一个

为什么程序员一定要写单元测试?

鱼皮 程序员鱼皮
向上滑动看下一个

您可能也对以下帖子感兴趣

文章有问题?点此查看未经处理的缓存