【JUnit实战3_32】第二十章:用 JUnit 5 进行测试驱动开发(TDD)(上)——将非 TDD 项目改造为 TDD 项目

《JUnit in Action》全新第3版封面截图

写在前面

从第 20 章开始,本书也进入最后一个板块的讲解。这一部分主要聚焦 JUnit 5 的几个高级话题:TDDBDD 以及测试金字塔策略。对于我这样的测试新手而言,每一章都十分精彩,让人耳目一新------TDDBDD 的概念听了很多,完整流程究竟是怎样的一直没有一个直观的认识。最后这个板块提供的就是诸如此类的完整示范。在国内普遍重交付、轻测试的大背景下,深入理解这些经典案例,对于后期充分利用 AI 智能体来提效软件测试,具有十分重要的意义。

第五部分:用 JUnit 5 开发应用程序

这一部分内容重点探讨如何将 JUnit 5 融入现代项目的日常开发工作------

第 20 章探讨如何使用当下流行的测试驱动开发(TDD)技术进行项目开发。

第 21 章探讨行为驱动开发(BDD)模式实施项目的具体方法。并给出完整开发案例。

第 22 章将借助 JUnit 5 构建测试金字塔策略(test pyramid strategy):从底层(单元测试)到上层(集成测试、系统测试和验收测试)的全方位测试方法。

第二十章:用 JUnit 5 进行测试驱动开发

本章概要

  • 普通项目改造为 TDD 项目的方法;
  • TDD 应用的重构方法;
  • TDD 在实现新功能特性中的应用。
    TDD helps you to pay attention to the right issues at the right time so you can make your designs cleaner, you can refine your designs as you learn. TDD enables you to gain confidence in the code over time.

测试驱动开发(TDD)有助于在正确的时间关注正确的问题,从而使设计更加清晰,并能在学习过程中不断优化设计。随着时间的推移 TDD 能让开发者建立起对代码的信心。

------ Kent Beck

本章没有堆砌 TDD 的观点理论,而是真正从实战角度出发,手把手地带领大家见识见识,究竟什么才是真正的测试驱动开发流程。

20.1 TDD 核心概念简介

测试驱动开发(Test-driven development) 是一种编程实践,它采用短周期循环的开发模式:先将需求转化为测试用例,再修改程序代码使测试通过。

TDD 倡导简约设计并强调安全性:其核心理念是追求 可运行的简洁代码(clean code that works)

TDD 的三个优势:

  • 代码由明确的目标驱动,确保精准满足应用需求;
  • 引入新功能的速度显著提升:既能更快实现新功能,又能大幅降低出 Bug 的可能;
  • 测试用例就能充当应用的文档(离不开对项目需求的深刻理解和前期良好的设计)。

TDD 实施的好坏还跟代码 重构 的质量密切相关。

重构(Refactoring 就是在不影响软件系统外部行为的前提下改善其内部结构的过程。

20.2 实战1:初始项目的搭建

即搭建一个非 TDD 的项目作为演示的起点。还是以航班管理应用为例,具体需求如下:

在航班 FlightPassenger 乘客实体交互过程中,航班添加乘客的规定如下 ------

  • 经济舱航班可供 VIP 乘客及普通乘客搭乘;
  • 商务舱航班仅供 VIP 乘客搭乘。

航班移除乘客的规定如下------

  • 普通乘客可被移除;
  • VIP 乘客不可移除。

具体示意图如下:

由此确定 FlightPassenger 的初始设计:航班实体具有 String 型字段 flightTypePassenger 也是航班实体的一个集合字段,具体类图如下:

根据需求定义 Passenger 实体类如下:

java 复制代码
public class Passenger {

    private String name;
    private boolean vip;

    public Passenger(String name, boolean vip) {
        this.name = name;
        this.vip = vip;
    }

    public String getName() {
        return name;
    }

    public boolean isVip() {
        return vip;
    }
}

航班实体类 Flight 如下:

java 复制代码
public class Flight {

    private String id;
    private List<Passenger> passengers = new ArrayList<Passenger>();
    private String flightType;

    public Flight(String id, String flightType) {
        this.id = id;
        this.flightType = flightType;
    }

    public String getId() {
        return id;
    }

    public List<Passenger> getPassengersList() {
        return Collections.unmodifiableList(passengers);
    }

    public String getFlightType() {
        return flightType;
    }

    public boolean addPassenger(Passenger passenger) {
        switch (flightType) {
            case "Economy":
                return passengers.add(passenger);
            case "Business":
                if (passenger.isVip()) {
                    return passengers.add(passenger);
                }
                return false;
            default:
                throw new RuntimeException("Unknown type: " + flightType);
        }

    }

    public boolean removePassenger(Passenger passenger) {
        switch (flightType) {
            case "Economy":
                if (!passenger.isVip()) {
                    return passengers.remove(passenger);
                }
                return false;
            case "Business":
                return false;
            default:
                throw new RuntimeException("Unknown type: " + flightType);
        }
    }
}

由于是非 TDD 项目,测试用例也最好不用 JUnit,于是新建一个 Airport 类模拟手动测试逻辑:

java 复制代码
public class Airport {

    public static void main(String[] args) {
        Flight economyFlight = new Flight("1", "Economy");
        Flight businessFlight = new Flight("2", "Business");

        Passenger james = new Passenger("James", true);
        Passenger mike = new Passenger("Mike", false);

        businessFlight.addPassenger(james);
        businessFlight.removePassenger(james);
        businessFlight.addPassenger(mike);
        economyFlight.addPassenger(mike);

        System.out.println("Business flight passengers list:");
        for (Passenger passenger : businessFlight.getPassengersList()) {
            System.out.println(passenger.getName());
        }

        System.out.println("Economy flight passengers list:");
        for (Passenger passenger : economyFlight.getPassengersList()) {
            System.out.println(passenger.getName());
        }
    }
}

运行 main 方法(符合预期):

20.3 实战2:改造为带单元测试的准 TDD 版

现在将原始的非 TDD 的项目改造为 TDD 版本。首先添加 JUnit 5 依赖。由于实测项目在用 Maven 命令行创建时已自带 JUnit 5,这一步可以跳过:

xml 复制代码
<dependencyManagement>
  <dependencies>
    <dependency>
      <groupId>org.junit</groupId>
      <artifactId>junit-bom</artifactId>
      <version>5.11.0</version>
      <type>pom</type>
      <scope>import</scope>
    </dependency>
  </dependencies>
</dependencyManagement>

<dependencies>
  <dependency>
    <groupId>org.junit.jupiter</groupId>
    <artifactId>junit-jupiter-api</artifactId>
    <scope>test</scope>
  </dependency>
  <!-- Optionally: parameterized tests support -->
  <dependency>
    <groupId>org.junit.jupiter</groupId>
    <artifactId>junit-jupiter-params</artifactId>
    <scope>test</scope>
  </dependency>
</dependencies>

对应的 Maven 创建命令为:

powershell 复制代码
mvn archetype:generate -DgroupId="com.manning.junitbook" -DartifactId="tdd-demo" -DarchetypeArtifactid="maven-artifact-mojo"

接着新建 JUnit 5 测试用例。根据乘法原理,两种航班类型与两类乘客共有 4 种交互类型,因此对应 4 个测试用例:

VIP 乘客(James) 普通乘客(Mike)
经济舱 可被添加、但不可被移除 可被添加、且可被移除
商务舱 可被添加、但不可被移除 不可被添加、且不可被移除

于是可利用 @Nest 注解和 @Display 注解创建如下测试类 AirportTest

java 复制代码
public class AirportTest {

    @DisplayName("Given there is an economy flight")
    @Nested
    class EconomyFlightTest {

        private Flight economyFlight;

        @BeforeEach
        void setUp() {
            economyFlight = new Flight("1", "Economy");
        }

        @Test
        public void testEconomyFlightRegularPassenger() {
            Passenger mike = new Passenger("Mike", false);

            assertEquals("1", economyFlight.getId());
            assertTrue(economyFlight.addPassenger(mike));
            assertEquals(1, economyFlight.getPassengersList().size());
            assertEquals("Mike", economyFlight.getPassengersList().get(0).getName());

            assertTrue(economyFlight.removePassenger(mike));
            assertEquals(0, economyFlight.getPassengersList().size());
        }

        @Test
        public void testEconomyFlightVipPassenger() {
            Passenger james = new Passenger("James", true);

            assertEquals("1", economyFlight.getId());
            assertTrue(economyFlight.addPassenger(james));
            assertEquals(1, economyFlight.getPassengersList().size());
            assertEquals("James", economyFlight.getPassengersList().get(0).getName());

            assertFalse(economyFlight.removePassenger(james));
            assertEquals(1, economyFlight.getPassengersList().size());
        }
    }

    @DisplayName("Given there is a business flight")
    @Nested
    class BusinessFlightTest {
        private Flight businessFlight;

        @BeforeEach
        void setUp() {
            businessFlight = new Flight("2", "Business");
        }

        @Test
        public void testBusinessFlightRegularPassenger() {
            Passenger mike = new Passenger("Mike", false);

            assertFalse(businessFlight.addPassenger(mike));
            assertEquals(0, businessFlight.getPassengersList().size());
            assertFalse(businessFlight.removePassenger(mike));
            assertEquals(0, businessFlight.getPassengersList().size());
        }

        @Test
        public void testBusinessFlightVipPassenger() {
            Passenger james = new Passenger("James", true);

            assertTrue(businessFlight.addPassenger(james));
            assertEquals(1, businessFlight.getPassengersList().size());
            assertFalse(businessFlight.removePassenger(james));
            assertEquals(1, businessFlight.getPassengersList().size());
        }
    }
}

运行结果:

考察测试覆盖率可以看到,Airport 类完全不参与测试,因此可以直接删除:

另外 Flight 的一些方法和代码行也有没被覆盖的情况(黄色划线部分),可以导出 HTML 报表查看详情:

如图所示,一些方法和分支并没有走到,说明源代码还有重构的空间。

(上篇完)

相关推荐
安冬的码畜日常3 小时前
【JUnit实战3_33】第二十章:用 JUnit 5 进行测试驱动开发(TDD)(下)——TDD 项目的重构过程及新功能的开发实战
测试工具·junit·单元测试·测试驱动开发·tdd·junit5·test-driven
测试老哥1 天前
软件测试之单元测试知识总结
自动化测试·软件测试·python·测试工具·职场和发展·单元测试·测试用例
m0_565611131 天前
Java高级特性:单元测试、反射、注解、动态代理
java·单元测试·log4j
稻香味秋天1 天前
单元测试指南
数据库·sqlserver·单元测试
郝开2 天前
Spring Boot 2.7.18(最终 2.x 系列版本)8 - 日志:Log4j2 基本概念;Log4j2 多环境日志配置策略
spring boot·单元测试·log4j
Sunlightʊə2 天前
2.登录页测试用例
运维·服务器·前端·功能测试·单元测试
慧都小项3 天前
软件测试工具Parasoft C/C++test如何通过桩函数实现多次调用返回不同值
单元测试·parasoft·桩函数
JosieBook3 天前
【SpringBoot】32 核心功能 - 单元测试 - JUnit5 单元测试中的嵌套测试与参数化测试详解
spring boot·单元测试·log4j
奔跑吧邓邓子5 天前
【C语言实战(79)】深入C语言单元测试:基于CUnit框架的实战指南
c语言·单元测试·实战·cunit