单元测验

什么是单元测验 ?

单元测验一般是指对一个函数或办法测验。单元测验的意图是验证每个单元的行为是否契合预期,并且在修正代码时能够快速检测到任何潜在的问题。经过编写测验用例,咱们能够验证这些模块在特定输入下是否产生正确的输出。单元测验的意图是保证每个模块在各种状况下都能正常运转。

写单元测验的优点

能够带来以下几个优点:

  1. 进步代码质量:单元测验能够咱们提早的发现代码中的潜在问题,例如边界条件、反常状况等,然后削减犯错的概率。
  2. 进步代码可保护性:单元测验能够协助开发人员了解代码的功用和完成细节,然后更容易保护和修正代码。
  3. 进步代码可靠性:修正代码后,能够经过单元测验能够协助开发人员验证代码的正确性,然后进步代码的可靠性。

写单元测验是一种杰出的软件开发实践,能够进步代码质量、可保护性和可靠性,一起也能够进步开发功率和支持继续集成和继续交给。

单元测验入门

上手单元测验,一般一起从静态测验(Static Test)开端,由于它简略,好了解,静态测验(Static Test)是指在编写测验用例时,咱们提早界说好一切的测验办法和测验数据。这些测验办法和数据在编译时就已经确认,不会在运转时发生变化。Junit 中的静态测验一般的惯例注解,如 @Test、@Before、@After 等。先来看看一组简略的静态测验示例。

首要,保证你的 pom.xml 文件包含 JUnit 的依靠:

<dependencies>
    <dependency>
        <groupId>org.junit.jupiter</groupId>
        <artifactId>junit-jupiter-api</artifactId>
        <version>5.8.0</version>
        <scope>test</scope>
    </dependency>
    <dependency>
        <groupId>org.junit.jupiter</groupId>
        <artifactId>junit-jupiter-engine</artifactId>
        <version>5.8.0</version>
        <scope>test</scope>
    </dependency>
</dependencies>

然后,创立一个简略的计算器类,一般这儿替换为你实际要测验的事务类:

public class SimpleCalculator {
    public int add(int a, int b) {
        return a + b;
    }
    public int subtract(int a, int b) {
        return a - b;
    }
}

然后在 /test 的相同目录下创立对应的测验类

import org.junit.jupiter.api.*;
import static org.junit.jupiter.api.Assertions.assertEquals;
public class SimpleCalculatorTest {
    // 在一切测验办法履行前,仅履行一次。这个办法需要是静态的。
    @BeforeAll
    static void setup() {
        System.out.println("BeforeAll - 初始化共享资源,例如数据库衔接");
    }
    // 在一切测验办法履行后,仅履行一次。这个办法需要是静态的。
    @AfterAll
    static void tearDown() {
        System.out.println("AfterAll - 整理共享资源,例如封闭数据库衔接");
    }
    // 在每个测验办法履行前,都会履行一次。用于设置测验办法所需的初始状况。
    @BeforeEach
    void init() {
        System.out.println("BeforeEach - 初始化测验实例所需的数据");
    }
    // 在每个测验办法履行后,都会履行一次。用于整理测验办法运用的资源。
    @AfterEach
    void cleanup() {
        System.out.println("AfterEach - 整理测验实例所用到的资源");
    }
    // 标注一个测验办法,用于测验某个功用。
    @Test
    void testAddition() {
        System.out.println("Test - 测验加法功用");
        SimpleCalculator calculator = new SimpleCalculator();
        assertEquals(5, calculator.add(2, 3), "2 + 3 应该等于 5");
    }
    // 再添加一个测验办法
    @Test
    void testSubtraction() {
        System.out.println("Test - 测验减法功用");
        SimpleCalculator calculator = new SimpleCalculator();
        assertEquals(1, calculator.subtract(3, 2), "3 - 2 应该等于 1");
    }
}

以上程序,能够看到 Junit 常用注解运用说明:

  • @BeforeAll:在一切测验办法履行前,仅履行一次。这个办法需要是静态的
  • @AfterAll:在一切测验办法履行后,仅履行一次。这个办法需要是静态的
  • @BeforeEach:在每个测验办法履行前,都会履行一次。用于设置测验办法所需的初始状况
  • @AfterEach:在每个测验办法履行后,都会履行一次。用于整理测验办法运用的资源
  • @Test:标注一个测验办法,用于测验某个功用

如果是 maven 项目,能够在目录下履行命令履行测验:

mvn test

输出成果:

[INFO] -------------------------------------------------------
[INFO]  T E S T S
[INFO] -------------------------------------------------------
[INFO] Running SimpleCalculatorTest
BeforeAll - 初始化共享资源,例如数据库衔接
BeforeEach - 初始化测验实例所需的数据
Test - 测验加法功用
AfterEach - 整理测验实例所用到的资源
BeforeEach - 初始化测验实例所需的数据
Test - 测验减法功用
AfterEach - 整理测验实例所用到的资源
AfterAll - 整理共享资源,例如封闭数据库衔接
[INFO] Tests run: 2, Failures: 0, Errors: 0, Skipped: 0, Time elapsed: 0.058 s - in SimpleCalculatorTest
[INFO] 
[INFO] Results:
[INFO]
[INFO] Tests run: 2, Failures: 0, Errors: 0, Skipped: 0
[INFO]
[INFO] ------------------------------------------------------------------------
[INFO] BUILD SUCCESS
[INFO] ------------------------------------------------------------------------

或许能够直接在 IDEA 中履行测验,如下:

提高代码质量的 5 个最佳实践

以上便是静态测验的简略示例

动态测验

动态测验(Dynamic Test):动态测验是指在编写测验用例时,咱们能够在运转时生成测验办法和测验数据。这些测验办法和数据在编译时不确认,而是在运转时依据特定条件或数据源动态生成。由于在静态单元测验中,由于测验样本数据有限,一般很难掩盖一切状况,掩盖率到了临界值就很难进步。JUnit 5 中引入动态测验,比较静态测验更杂乱,当然也更灵敏,也更适合杂乱的场景。接下来经过一个简略的示例来展现动态测验和静态测验的差异,咱们创立 MyStringUtil 类,它有一个办法 reverse() 用于回转字符串,如下:

public class MyStringUtil {
    public String reverse(String input) {
        if (input == null) {
            return null;
        }
        return new StringBuilder(input).reverse().toString();
    }
}

在静态测验类中,咱们运用 @Test 界说 3 个办法来尝试掩盖 reverse() 或许得多种状况:

import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.assertEquals;
public class MyStringUtilStaticTest {
    private MyStringUtil stringUtil = new MyStringUtil();
    @Test
    void reverseString() {
        // 回转字符串 'hello'
        assertEquals("olleh", stringUtil.reverse("hello"));
    }
    @Test
    void reverseEmptyString() {
        // 回转空字符串
        assertEquals("", stringUtil.reverse(""));
    }
    @Test
    void handleNullString() {
        // 处理 null 字符串
        assertEquals(null, stringUtil.reverse(null));
    }
}

然后用动态测验来完成同样的测验用例:

import org.junit.jupiter.api.DynamicTest;
import org.junit.jupiter.api.TestFactory;
import java.util.Arrays;
import java.util.Collection;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.DynamicTest.dynamicTest;
public class MyStringUtilDynamicTest {
    private MyStringUtil stringUtil = new MyStringUtil();
    // 运用 @TestFactory 注解界说了一个动态测验工厂办法 reverseStringDynamicTests()
    // 工厂办法返回一个 Collection<DynamicTest>
    @TestFactory
    Collection<DynamicTest> reverseStringDynamicTests() {
        // 包含了 3 个动态测验用例,每个测验用例运用 dynamicTest() 办法创立
        return Arrays.asList(
                dynamicTest("动态测验:回转字符串 'hello'", () -> assertEquals("olleh", stringUtil.reverse("hello"))),
                dynamicTest("动态测验:回转空字符串", () -> assertEquals("", stringUtil.reverse(""))),
                dynamicTest("动态测验:处理 null 字符串", () -> assertEquals(null, stringUtil.reverse(null)))
        );
    }
}

在动态测验类中逻辑如下:

  1. 运用 @TestFactory 注解界说了一个动态测验工厂办法 reverseStringDynamicTests()
  2. 工厂办法返回一个 Collection<DynamicTest>,其间包含了 3 个动态测验用例。
  3. 每个测验用例运用 dynamicTest() 办法创立。

以上便是根本的单元测验运用办法,关于 Junit 5 的具体运用并不打算在这儿详解,有兴趣能够去参阅 Junit 5 的官方文档

单元测验 + Dbc

编写单元测验需要尽或许的遵从 契约式规划 (Design By Contract, DbC) 代码风格,关于契约式规划能够参阅以下的描述:

契约式规划 (Design By Contract, DbC) 是一种软件开发办法,它强调在软件开发中关于每个模块或许函数,应该清晰界说其输入和输出的约好(契约)。这些契约能够包含前置条件(preconditions)和后置条件(postconditions),以及或许发生的反常状况。在代码完成时,有必要满足这些约好,不然就会引发过错或许反常。

这样说或许比较笼统,能够经过以下的示例代码来了解,如何运用断语来完成契约式规划:

public class BankAccount {
    private double balance;
    public BankAccount(double balance) {
        this.balance = balance;
    }
    public void withdraw(double amount) {
        assert amount > 0 : "Amount must be positive";
        assert amount <= balance : "Insufficient balance";
        balance -= amount;
        assert balance >= 0 : "Balance can't be negative";
    }
    public double getBalance() {
        return balance;
    }
}

在这个示例中,咱们运用了 Java 中的断语(assertion)来完成契约式规划。具体来说:

  • assert amount > 0 : "Amount must be positive"; 表明取款金额 amount 有必要大于 0
  • assert amount <= balance : "Insufficient balance"; 表明取款金额 amount 有必要小于等于账户余额 balance
  • assert balance >= 0 : "Balance can't be negative"; 表明取款完成后,账户余额 balance 的值应该为非负数

能够经过运用 JVM 的 -ea 参数来敞开断语功用,不过由于启用 Java 本地断语很费事,Guava 团队添加一个始终启用的用来替换断语的 Verify 类。他们主张静态导入 Verify 办法。用法和断语差不多,这儿就不过多赘述了。

测验驱动开发 TDD

测验驱动开发(TDD)是一种软件开发办法,也是我个人十分推重的一种软件开发办法,便是在编写代码之前编写单元测验。TDD 的核心思想是在编写代码之前,先编写测验用例。开发人员在编写代码前先思考预期成果,以便能够编写测验用例。接着开发人员编写足够简略的代码来经过测验用例,再对代码进行重构以进步质量和可保护性。

如图:

提高代码质量的 5 个最佳实践

作为 TDD 的长期实践者,我总结 TDD 能带来的优点如下:

  1. 进步可保护性:一般咱们不敢去保护一段代码的原因是没有测验,TDD 树立的完善测验,能够为重构代码供给保障
  2. 更快速的开发:很多开发总想着完成功用后再去补测验,但一般功用完成后,还会有更多的功用,所以尽量在功用开端前先写测验
  3. 更高质量的交给:这儿就不用多说了,经过测验的代码和没有测验的代码,是完全不一样的。未经测验的代码底子不具备上生产的条件

日志

充足的日志能够协助开发人员更好地了解程序的运转状况。经过查看日志,能够了解程序中发生了什么事情,以及在哪里发生了问题。这能够协助开发人员更快地找到和处理问题,然后进步程序的稳定性和可靠性。此外,日志还能够用于盯梢程序的性能和行为,以便进行优化和改进。

日志输出

经过以下是打印简略日志的示例:

  1. 首要,你需要在项目中添加SLF4J的依靠。你能够在Maven或Gradle中添加以下依靠:
<dependency>
    <groupId>org.slf4j</groupId>
    <artifactId>slf4j-api</artifactId>
    <version>1.7.30</version>
</dependency>
  1. 接下来,你需要选择一个SLF4J的完成,例如Logback或Log4j2,并将其添加到项目中。你能够在Maven或Gradle中添加以下依靠:
<dependency>
    <groupId>ch.qos.logback</groupId>
    <artifactId>logback-classic</artifactId>
    <version>1.2.3</version>
</dependency>
  1. 在代码中,你能够运用以下代码打印Hello World:
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
public class HelloWorld {
    private static final Logger logger = LoggerFactory.getLogger(HelloWorld.class);
    public static void main(String[] args) {
        logger.info("Hello World");
    }
}

这将运用SLF4J打印一条信息,其间包含“Hello World”字符串。你能够在操控台或日志文件中查看此信息。

日志等级

首要是为了协助开发人员更好地操控和管理日志输出。SLF4J 界说了多个日志等级:

日志等级 内容
TRACE 用于盯梢程序的细节信息,一般用于调试。
DEBUG 用于调试程序,输出程序中的详细信息,例如变量的值、办法的调用等。
INFO 用于输出程序的运转状况信息,例如程序启动、封闭、衔接数据库等。
WARN 用于输出正告信息,表明程序或许存在潜在的问题,但不会影响程序的正常运转。
ERROR 用于输犯过错信息,表明程序发生了过错,包含致命过错。

不同的日志等级用于记载不同的信息。这样做的意图不仅能够削减不用要的日志输出和文件巨细,还能够供给快速定位的能力,例如开发环境一般运用 TRACE、DEBUG 日志,生产环境一般运用 INFO,WARN 日志等。这些信息都能够在 logback.xml 日志装备文件里边装备。

日志装备

以下是一个根本的 logback 装备文件示例,该装备文件将日志输出到操控台和文件中:

<configuration>
  <appender name="CONSOLE" class="ch.qos.logback.core.ConsoleAppender">
    <encoder>
      <pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n</pattern>
    </encoder>
  </appender>
  <appender name="FILE" class="ch.qos.logback.core.rolling.RollingFileAppender">
    <file>/var/log/myapp.log</file>
    <rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
      <fileNamePattern>/var/log/myapp.%d{yyyy-MM-dd}.log</fileNamePattern>
      <maxHistory>7</maxHistory>
    </rollingPolicy>
    <encoder>
      <pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n</pattern>
    </encoder>
  </appender>
  <root level="INFO">
    <appender-ref ref="CONSOLE" />
    <appender-ref ref="FILE" />
  </root>
</configuration>

在此装备文件中,界说了两个 appender:

  1. 一个用于将日志输出到操控台(CONSOLE)
  2. 一个用于将日志输出到文件(FILE)

操控台的日志格式运用了 pattern 格式化方式,而文件的日志运用了 RollingFileAppender 完成每日轮换,并界说了最多保存 7 天的日志历史。一起,界说了一个根(root)等级为 INFO 的 logger,它会将日志输出到 CONSOLE 和 FILE 两个 appender 中,其他日志等级(TRACE、DEBUG、WARN、ERROR)则按照默许装备输出到根 logger 中。

代码静态查看

在 Java 静态扫描东西能够协助开发人员在开发过程中及时发现和修复代码中的问题和过错,然后进步代码质量和安全性。这些静态扫描东西还能够约束代码风格,在团队协助开发中,一致的风格,能够增强团队协作和沟通,能够添加代码的可读性,可保护性,还削减不用要的讨论和争议,有利于后续的 CodeReview 进展。下面是一些常用的 Java 静态扫描东西:

东西名称 Github 地址
FindBugs github.com/findbugspro…
PMD github.com/pmd/pmd
Checkstyle github.com/checkstyle/…
SonarQube github.com/SonarSource…
IntelliJ IDEA github.com/JetBrains/i…

拜访它们的 Github 地址也供给了更多的信息和支持,能够协助开发人员更好地了解和运用这些东西。另外,主张在开发过程中,将这些东西集成到继续集成和继续交给的流程中,以便自动化地进行代码查看和修复。

Code Review

人工的 CodeReview 一般是开发流程的最终一步,为什么前面做了那么多测验和查看东西,到最终还需要人工查看呢 ?

由于静态扫描东西一般只能查看一些简略的问题和过错,比较人工查看它存在以下局限性:

  1. 只能查看例如语法过错、安全漏洞常见的过错等。
  2. 只能查看问题和过错,但无法给出更好的主张和处理方案。(它供给的通用处理方案未必是最好的)
  3. 静态扫描东西只能查看代码是否契合特定的规范和规范,但无法保证代码的质量和可读性。

比较机器扫描,人工 Code Review 能够供给以下不可替代的优势:

  1. 能够发现更杂乱的问题,例如:事务逻辑的问题、不合理的规划、不用要的杂乱性等
  2. 比较机器的主张,人工 Code Review 能够依据经验和知识,供给更好的处理方案和主张
  3. 能够促进团队协作和学习,经过分享和讨论代码,能够进步开发人员的技能和知识,并进步团队的凝聚力和功率。

综上所述,尽管静态扫描东西能够协助开发人员自动化地发现代码中的问题和过错,但 Code Review 仍然是一种必要的软件开发实践,能够进步代码的质量、可读性和可保护性,一起也能够促进团队协作和学习。因而,主张在开发过程中,将人工 Code Review 和静态扫描东西结合起来,以便更全面和深化地审阅和审查代码。

总结

在现代软件开发中,单元测验、TDD、日志、静态查看扫描和人工 Code Review 都是必要的实践,能够协助开发人员保证软件质量、进步代码可读性和可保护性,并促进团队协作和学习。

首要,单元测验是一种测验办法,用于测验代码的根本单元,例如函数、办法等。单元测验能够协助开发人员及早发现和处理代码中的问题和过错,然后进步代码质量和可靠性。一起,单元测验还能够进步代码的可读性和可保护性,使代码更易于了解和修正。

其次,TDD(Test-Driven Development,测验驱动开发)是一种开发办法,要求在编写代码之前先编写测验用例。经过运用 TDD,开发人员能够更好地了解代码需求和规范,防止代码中的过错和问题,并进步代码的可读性和可保护性。

第三,日志是一种记载程序运转时状况和信息的办法。日志能够协助开发人员调试程序,发现潜在的过错和问题,并供给更好的过错处理和处理方案。一起,日志还能够记载程序运转时的性能和状况,然后协助开发人员分析和优化程序性能。

第四,静态查看扫描东西是一种自动化的代码审阅和审查东西,能够协助开发人员及早发现和处理代码中的问题和过错。经过运用静态查看扫描东西,开发人员能够更全面地查看代码中的问题和过错,并进步代码质量和可读性。

最终,人工 Code Review 是一种手动审阅和审查代码的办法,能够更深化地查看代码中的问题和过错,并供给更好的处理方案和主张。人工 Code Review 能够促进团队协作和学习,进步代码质量和可读性,一起还能够遵从特定的编码规范和规范。

综上所述,单元测验、TDD、日志、静态查看扫描和人工 Code Review 都是必要的软件开发实践,能够进步代码质量、可读性和可保护性,并促进团队协作和学习。在进行软件开发时,应该尽或许地遵从这些实践,并运用相应的东西和技能进行代码审阅和测验。