为了账号安全,请及时绑定邮箱和手机立即绑定

Java 23与SpringBoot 3.3.4:使用JUnit 5和Mockito进行AI驱动的测试自动化(第三部分)

在前两节中,我们探讨了Gen AI驱动或增强的工具如何能够革新测试自动化。接下来,我们将进一步探讨标准的测试自动化工具,并看看GitHub Copilot如何加速代码生成。

  1. 第一部分:Java 23, Spring Boot 3.3.4 — AI 驱动的测试生成:在介绍部分,我们探讨了云原生架构格局及其与自动化测试的交集。我们介绍了 Diffblue Cover,这是一个基于 AI 的工具,用于自动生成测试用例,展示了其简化测试流程的潜力。

  2. 第二部分:Java 23, Spring Boot 3.3.4 — 使用 GitHub Copilot 进行增强测试:在第二部分中,我们专注于利用 GitHub Copilot 自动生成简单和复杂的测试用例。虽然它不能生成完整的代码库,但其显著提升了工作效率,估计提升幅度在 65-80% 之间,使开发人员能够加快测试脚本的创建。

第三部分:Java 23, SpringBoot 3.3.4: AI驱动的:JUnit 5, Mockito — 第三部分:这一部分,我们将深入探讨 JUnit 5Mockito,探索它们的高级功能,从而帮助我们创建或增强测试用例,以自动化构建流程。测试自动化是快速软件交付的核心;没有测试自动化,实现 DevOps 的目标将变得不可能。

这是用Open AI做的图。

让我们转向 Google Trends 分析自动化测试平台领域的现状。数据显示,TestNG 在数据中显示出了对 JUnit 5 的主导地位。尽管如此,我们将从 JUnit 5 开始我们的旅程,最终也会探讨 Mockito,从而全面了解这些工具。

你可以在这里查看:Google 数据趋势 — JUnit 5, TestNG 和 Spock 测试框架

1. JUnit 5 平台框架

JUnit 5 ,在2017年9月发布,是JUnit测试框架的最新版本,旨在利用现代Java特性,如 lambda表达式、流和Java 8+ API ,。其模块化架构主要包括三个关键组件:

JUnit PlatformJUnit JupiterJUnit Vintage 支持无缝运行 JUnit 5 和旧版的 JUnit 3/4。JUnit 5 已经与 Spring Framework 深度集成,特别是在如 @SpringBootTest 这样的注解和 SpringExtension 的帮助下,成为测试基于 Spring 应用程序的首选。

它在开源社群中的采用非常广泛,这得益于其直观易用的API、对参数化测试的强大支持以及与现代构建工具如Gradle和Maven的良好兼容性,从而使其成为Java测试事实上的标准。

行业倾向
  • JUnit 5 :由于其更现代的特性及更简洁的API,在新的项目开源社区中的贡献、以及基于Spring的项目中更为流行。
  • TestNG :在需要灵活的测试配置的遗留系统和团队中仍然在这些领域中占有重要地位。
社区和生态系统
  • JUnit 5 因此,由于它已经成为行业标准,特别是与现代 Java 特性(Java 8+)的良好兼容性,受益于更大的社区支持。
  • TestNG 特别是在像 Selenium 这样的测试框架中拥有一群忠实的追随者,其高级配置功能尤其受到青睐。
摘要
  • 对于新项目JUnit 5 是更受欢迎的选择,因为它采用了现代化的设计、更好的工具支持,并且在诸如 Spring Boot 的框架中被广泛使用。
  • 对于旧系统或复杂的测试设置TestNG 仍然有很多优势,因为它提供了强大的依赖管理和并行执行能力。

什么是被测试的系统?在软件开发和测试中,被测试系统指的是需要进行测试的目标系统。

系统-under-test (SUT),即被测试的特定组件、模块或功能。SUT是软件应用中的测试的主要对象。不论是单元测试契约测试集成测试还是端到端测试,SUT都是测试的主要对象。测试SUT的目标是确保其在各种条件下正常运行。

SUT的关键特性:

  • 范围: 这可以是一个单一的类、一个方法,或者是更广泛的组件,取决于测试的层次。
  • 重点: 测试是为了验证SUT的行为是否符合定义的要求或预期的输出结果。
  • 隔离: 在单元测试中,SUT会从外部依赖(如数据库或API)中隔离出来进行测试,以确保测试纯粹关注SUT的行为本身。

以下图展示了 JUnit 5 单元测试的生命周期以及测试阶段、注解和 被测系统(SUT,即系统被测) 之间的关系。

出处:微服务测试策略

例如: 被测系统通常通过 Java 中的一个类或方法来表示。JUnit 测试方法被编写用来验证这些组件的功能。

    class Calculator {  
        int add(int a, int b) {  
            return a + b;  
        }  
    }  

    class CalculatorTest {  
        @Test  
        void testAdd() {  
            Calculator calculator = new Calculator(); // 被测系统 (SUT)  
            int result = calculator.add(2, 3);  
            assertEquals(5, result); // 对被测系统行为进行断言  
        }  
    }
JUnit 5 测试生命周期组件介绍

@BeforeAll:在所有测试方法之前运行:@BeforeAll

  • 此方法在所有测试用例运行之前只运行一次。
  • 通常用于全局设置,比如初始化所有测试都会用到的共享资源(例如数据库连接)。

@BeforeEach: (每个测试前:)

  • 此方法在每个测试用例运行之前执行,表示其作用是为每个单独的测试用例做准备。
  • 用以设定特定于每个测试用例的SUT或测试环境。SUT即系统UnderTest(被测试系统)。

@测试:

  • 这是测试套件的核心部分,实现具体的测试逻辑,用来验证SUT的特定行为。

@每次执行后

  • 这种方法在每个测试用例结束后运行。
  • 用于清理资源并在每个测试后重置状态,以确保测试之间的隔离。

@AfterAll:

  • 此方法在测试套件中的所有测试用例运行之后执行一次。
  • 通常用于清理任务,比如关闭连接或清理共享资源。
JUnit 5: 测试生命周期
  1. 全局初始化 (@BeforeAll):仅一次性为整个测试套件初始化共享资源。
  2. 测试准备(@BeforeEach):为每个单独的测试准备测试环境或系统待测(SUT)。
  3. 测试执行(@Test):通过断言执行测试用例,验证SUT的行为。
  4. 后测试清理(@AfterEach):执行清理或重置任务,确保测试之间的隔离。
  5. 全局清理 (@AfterAll):在所有测试完成后释放共享资源。
JUnit 5 注解(Annotations)

下面的图展示了关键的JUnit 5 注解,如,对于编写、组织和运行测试非常重要。

来自:微服务测试策略

核心测试标注

@测试

  • 将一个 测试方法 标记为测试用例。
  • 当运行测试套件时,这个测试方法就会被执行。
    @Test  
    void 测试加法() {  
        assertEquals(5, 2 + 3);  
    }

@重复测试:

  • 多次执行测试方法。
  • 这种功能用于压力测试或验证重复执行的结果。
    @RepeatedTest(5)  
    void repeatedTest() {  
        System.out.println("这个测试会运行五次。");  
    }

@NestedTest:

用来将相关的测试用例归类到一个嵌套类,从而提升测试的可读性并帮助逻辑分组。

    @Nested  
    class 内部类测试 {  
        @Test  
        void nestedTest() {  
            assertTrue(true);  
        }  
    }
@ParameterizedTest 的参数源
  • 允许用不同的输入数据运行相同的测试。
  • 需要一个数据源,如 @ValueSource 或 @CsvSource。

@ValueSource: 为测试方法提供一个数组。

    @ParameterizedTest  
    @ValueSource(strings = {"Apple", "Samsung"})  
    void testWithStrings(String company) {  
        assertNotNull(company);  
    }

枚举来源: 这个注解提供的是枚举类型中的值。

    @参数化测试  
    @枚举源(TimeUnit.class) // 枚举时间单位
    void 测试时间单位(TimeUnit 单位) {  
        assertNotNull(unit); // 确保单位不为空
    }

这注解用于从静态工厂方法提供值。

    static Stream<String> getProducts() {  
        return Stream.of("iPhone 16", "iPad Pro", "Apple Watch");  
    }  

    @ParameterizedTest  
    @MethodSource("getProducts")  
    void testWithMethodSource(String product) {  
        assertNotNull(product);  
    }

@CsvSource: 为多个参数提供 CSV 格式的值:

    @参数化测试  
    @Csv数据源({"1, one", "2, two"})  
    void testWithCsvSource(int 数字, String 单词) {  
        assertEquals(数字, 单词.length());  
    }

@CsvFileSource: 这个注解用于从文件中读取CSV格式的数据。

    @ParameterizedTest  
    @CsvFileSource(resources = "/test-data.csv")  
    void 测试Csv文件(int number, String word) {  
        assertNotNull(word);  
    }
注解

显示名:

  • 给测试用例自定义一个名称。
  • 让测试输出更加详细。
    @Test  
    @DisplayName("测试两个数相加")  
    void additionTest() {  
        // assertEquals(7, 4 + 3); 用于验证计算结果是否正确
        assertEquals(7, 4 + 3);  
    }

@标签: 将测试分类和筛选,以便执行特定的测试。

    @Test  
    @Tag("功能")  
    void 测试() {  
        assertTrue(true);  
    }

@Order: 定义类内测试方法的执行顺序。

    /**

* 第一个测试方法,顺序为1
     */
    @Test  
    @Order(1)  
    void firstTest() {  
        assertTrue(true);  
    }

@已禁用:

  • 这可以禁止运行测试方法或类。
  • 这适合暂时跳过某些测试。
    @Test  
    @Disabled("未实现")  
    void disabledTest() {  
        assertTrue(false);  
    }
注解的目的\n(注解的用途和意义)

这些注解增强了JUnit 5中测试用例的灵活性、模块化和可读性,并通过允许开发人员做到以下几点来实现。

  • 编写动态测试(使用@ParameterizedTest注解)。
  • 逻辑地组织和分组测试(使用@NestedTest注解)。
  • 选择性地运行测试(使用@Tag@Disabled 注解)。
  • 提供清晰且有意义的输出(使用@DisplayName注解)。

通过这些注解,开发人员可以创建更可靠、更易维护且更高效的测试套件。
来源:对于JUnit 5 注解 的理解更加深入。

断言(在测试中使用的断言) — JUnit 5 (或其他工具) 和其他工具

断言是任何测试框架的核心部分,用于验证被测系统的预期行为。各种库如JUnitHamcrestGoogle TruthAssertJ提供了多种不同的断言编写方式,每种库都有自己独特的特性和语法。

JUnit 断言测试

JUnit(尤其是JUnit 5)提供了一套简洁且有效的断言来验证测试的输出。这些断言简单有效,适用于单元测试。
参考来源: JUnit 5 断言

关键要点:

  • 相等检查: assertEquals(预期值, 实际值):检查两个值是否相等。
  • 不相等检查: assertNotEquals(预期值, 实际值):确保两个值不相同。
  • 布尔值检查: assertTrue(条件) / assertFalse(条件):验证条件是否为真或假。
  • 空值检查: assertNull(对象) / assertNotNull(对象):确保对象为 null 或非 null 值。
  • 异常检查: assertThrows(预期异常, 可执行代码):验证是否抛出了特定异常。
    @Test  
    void testAssertions() {  
        assertEquals(5, 2 + 3, "加法结果不对");  
        assertTrue(3 > 1, "这应该是对的");  
        assertThrows(IllegalArgumentException.class, () -> {  
            throw new IllegalArgumentException("无效参数");  // (这将抛出异常)
        });  // (这将抛出异常)
    }
Hamcrest 断言匹配

Hamcrest 是一个专注于 基于匹配器的断言方式 的库,它提供了一种 更灵活且易读的方式来编写断言。它通常与 JUnit 一起使用,以提高测试的可读性。

主要特点

  • 提供了用于复杂断言(如集合类和字符串)的匹配器。
  • 通过类似自然语言的语法(如 assertThat)提高可读性。

一些常用的匹配规则:

  • 平等: is(equalTo(值)):断言相等。
  • 不等: not(值):断言不等。
  • 包含: containsString(子字符串):检查字符串是否包含特定的子字符串。
  • 包含项: hasItem(值):检查集合是否包含特定的项。
  • 每个项: everyItem(greaterThan(值)):验证集合中的每个元素是否都满足某个条件。
    import static org.hamcrest.MatcherAssert.assertThat;  
    import static org.hamcrest.Matchers.*;  

        @Test  
        void testHamcrestAssertions() {  
            // 示例 1:使用 'is' 检查相等性  
            int result = 5 + 3;  
            assertThat("结果应该是 8", result, is(8));  

            // 示例 2:使用 'not' 检查不相等性  
            String name = "JUnit";  
            assertThat("名字不应该为 'TestNG'", name, is(not("TestNG")));  

            // 示例 3:使用 'contains' 检查集合中的元素  
            List<String> fruits = Arrays.asList("Apple", "Banana", "Cherry");  
            assertThat("水果列表应该包含 'Banana'", fruits, contains("Apple", "Banana", "Cherry"));  

            // 示例 4:使用 'containsString' 检查子字符串  
            String sentence = "JUnit 是很好的测试工具。";  
            assertThat("句子应该包含子字符串 '测试'", sentence, containsString("测试"));  
        }
谷歌事实声明

Google Truth 是由 Google 开发的一个流畅的断言库,强调 易读清晰。其语法设计得自然流畅,易于阅读,让测试更容易理解。

主要特点:

  • 流畅且可链接的语法。
  • 内置支持集合、字符串和异常等。
  • 支持用户自定义断言进行扩展。

常见的说法:

  • 相等性: assertThat(value).isEqualTo(expected);

  • 不相等性: assertThat(value).isNotEqualTo(unexpected);

  • 包含: assertThat(collection).contains(value);

  • 包含: assertThat(string).contains(substring);

  • assertThat(exception).hasMessageContaining("message"): 验证异常消息是否包含。
    import static com.google.common.truth.Truth.assertThat;  

    @Test  
    void testGoogleTruthAssertions() {  
        assertThat("Google Truth").contains("Truth");  // 检查"Google Truth"中是否包含"Truth"
        assertThat(5).isEqualTo(5);  // 检查5是否等于5
        assertThat(Arrays.asList(1, 2, 3)).contains(2);  // 检查列表[1, 2, 3]中是否包含2
    }
AssertJ 断言库

这是一个强大的且灵活的库,用于在Java中编写流畅且易读的断言。它通过提供一个流畅的API的,和针对各种对象(例如集合、映射、日期、字符串、数字和异常)的丰富断言,来提高测试的可读性。

AssertJ 致力于改进 JUnit 和其他如 Hamcrest 的库所提供的传统断言的不足。

1. 基本陈述

  • 相等 : assertThat(actual).isEqualTo(expected)
  • 不相等 : assertThat(actual).isNotEqualTo(非预期)
  • 空值检验 : assertThat(actual).isNull(); / assertThat(actual).isNotNull()
  • 布尔检查 : assertThat(condition).isTrue(); / assertThat(condition).isFalse()

2. 字符串断言检查

  • 包含 : assertThat(string).contains(substring);

  • 不包含 : assertThat(string).doesNotContain(substring);

  • 以...为开头 : assertThat(string).startsWith(prefix);

  • 以...为结尾 : assertThat(string).endsWith(suffix);

  • 符合正则表达式 : assertThat(string).matches(regex);

  • 长度是 : assertThat(string).hasSize(size);

  • 是空的 : assertThat(string).isEmpty();

  • 不是空的 : assertThat(string).isNotEmpty();

3. 数字声明

  • 相等 : assertThat(number).isEqualTo(expected);
  • 不相等 : assertThat(number).isNotEqualTo(unexpected);
  • 大于 : assertThat(number).isGreaterThan(value);
  • 小于 : assertThat(number).isLessThan(value);
  • 在...和...之间 : assertThat(number).isBetween(start, end);
  • 接近于 : assertThat(number).isCloseTo(value, offset);

4. 关于日期和时间的断定

  • 早于 : assertThat(date).isBefore(otherDate);

  • 晚于 : assertThat(date).isAfter(otherDate);

  • 在...和...之间 : assertThat(date).isBetween(startDate, endDate);

  • 等于预期的 : assertThat(date).isEqualTo(expectedDate);

5. 数组断言

  • 包含 : assertThat(collection).contains(元素);

  • 包含确切地是这些元素 : assertThat(collection).containsExactly(元素);

  • 至少包含其中任意一个 : assertThat(collection).containsAnyOf(元素);

  • 不包含 : assertThat(collection).doesNotContain(元素);

  • 大小为 : assertThat(collection).hasSize(大小);

  • 为空 : assertThat(collection).isEmpty();

  • 不为空 : assertThat(collection).isNotEmpty();

6. 地图声明

  • 包含键:assertThat(map).包含键(key);

  • 包含值:assertThat(map).包含值(value);

  • 包含项:assertThat(map).包含项(key, value);

  • 不包含键:assertThat(map).不包含键(key);

  • 不包含值:assertThat(map).不包含值(value);

7. 流断言(Stream 断言)

  • 包含 : assertThat(stream).contains(element);
  • 不包括 : assertThat(stream).doesNotContain(element);
  • 不为空 : assertThat(stream).isNotEmpty();
  • 大小为 : assertThat(stream).hasSize(size);
    import static org.assertj.core.api.Assertions.assertThat;  
    // 测试用例  

        @DisplayName("1. 基本断言测试")  
        @Test  
        void basicAssertions() {  
            int result = 5 + 3;  

            assertThat(result)  
                    .isEqualTo(8)  
                    .isNotEqualTo(9)  
                    .isGreaterThan(7)  
                    .isLessThan(10);  
        }  

        @DisplayName("2. 字符串断言")  
        @Test  
        void stringAssertions() {  
            String message = "Welcome to AssertJ with JUnit 5";  

            assertThat(message)  
                    .isNotEmpty()  
                    .contains("AssertJ")  
                    .startsWith("Welcome")  
                    .endsWith("JUnit 5")  
                    .doesNotContain("Hamcrest");  
        }  

        @DisplayName("3. 日期断言")  
        @Test  
        void dateAssertion() {  
            LocalDate today = LocalDate.now();  
            LocalDate tomorrow = today.plusDays(1);  

            assertThat(today).isBefore(tomorrow);  
            assertThat(tomorrow).isAfter(today);  
        }  

        @DisplayName("4. 集合断言")  
        @Test  
        void collectionAssertions() {  
            List<String> fruits = List.of("Apple", "Banana", "Cherry");  

            assertThat(fruits)  
                    .isNotEmpty()  
                    .contains("Banana")  
                    .containsExactly("Apple", "Banana", "Cherry")  
                    .doesNotContain("Orange")  
                    .containsAnyOf("Grape", "Cherry");  
        }  

        @DisplayName("5. 映射断言")  
        @Test  
        void mapAssertions() {  
            Map<String, Integer> scores = Map.of("Alice", 90, "Bob", 85, "Charlie", 95);  

            assertThat(scores)  
                    .isNotEmpty()  
                    .containsKey("Alice")  
                    .containsEntry("Bob", 85)  
                    .doesNotContainKey("Eve")  
                    .containsValues(90, 95);  
        }  

        @DisplayName("6. 流断言")  
        @Test  
        void streamAssertions() {  
            Stream<String> stream = Stream.of("one", "two", "three");  

            assertThat(stream)  
                    .isNotEmpty()  
                    .contains("two", "three")  
                    .doesNotContain("four");  
        }  

        @DisplayName("7. 组合断言")  
        @Test  
        void combinedAssertions() {  
            String message = "Hello, AssertJ!";  

            assertThat(message)  
                    .isNotNull()  
                    .startsWith("Hello")  
                    .contains("AssertJ")  
                    .endsWith("!")  
                    .doesNotContain("JUnit 4");  
        }
断言库对比: JUnit, Hamcrest库, Google Truth库, AssertJ:

总结

  • JUnit 5 非常适合简单的、无需配置的断言。
  • Hamcrest 在需要基于匹配器的断言时特别理想,尤其是在处理集合或复杂条件时。
  • Google Truth 提供了由 Google 支持的、可读性很强的流畅 API。
  • AssertJ 是功能最丰富、最灵活的,支持各种数据类型,适用于复杂的情况。

选择框架时,请考虑项目的复杂性、团队的偏好和可读性要求。你可以查看GitHub 仓库中的示例。

JUnit 假定

在 JUnit 5 中,一个 前提条件测试继续的前提条件。如果前提条件失败,测试会被跳过而不是标记为失败。

假设通常用于根据条件执行测试,基于特定的运行时条件(如操作系统、环境变量或特定设置)。来源:JUnit 假设

  • assumeTrue(条件为真):仅当条件为真时执行测试。
  • assumeFalse(条件为假):仅当条件为假时执行测试。
  • assumingThat(条件, 可执行代码块):如果条件为真则执行一段代码,但测试的其余部分会继续执行,不管条件是否为真。
        @DisplayName("1. 假设测试 - 假设 CI == ENV")  
        @Order(1)  
        @Test  
        void testOnlyOnCiServer() {  
            assumeTrue("CI".equals(System.getenv("ENV")));  
            // 测试的其余代码  
            assertTrue(true);  
        }  

        @DisplayName("2. 假设测试 - 假设 DEV == ENV")  
        @Order(2)  
        @Test  
        void testOnlyOnDeveloperWorkstation() {  
            assumeTrue("DEV".equals(System.getenv("ENV")),  
                    () -> "跳过测试: 不在开发人员工作站上");  
            // 测试的其余代码  
            assertTrue(true);  
        }  

        @DisplayName("3. 假设测试 - 假设 CI == ENV")  
        @Order(3)  
        @Test  
        void testInAllEnvironments() {  
            assumingThat("CI".equals(System.getenv("ENV")),  
                    () -> {  
                        // 仅在 CI 服务器中执行这些断言  
                        assertEquals(2, calculator.divide(4, 2));  
                    });  

            // 在所有环境里执行这些断言  
            assertEquals(42, calculator.multiply(6, 7));  
        }
JUnit 的注解及其使用方法

下面的图表展示了如何根据您的技术及业务需求来整理标签。您可以根据需要创建任意数量的标签。在这个例子中,我们将标签大致分成两大类标签。

  1. 功能性的需求
  2. 非功能性需求 (NFR)

来源:微服务测试策略

NFR 可以进一步细分为四个子类别。

  • 2.1 安全
  • 2.2 性能
  • 2.3 易用
  • 2.4 可访问性

另外,该性能类别被进一步细分为:

  • 2.2.1负载测试
  • 2.2.2压力测试
使用标签来筛选测试用例
  • 标记 允许你在构建时选择性地运行特定测试,提供了更大的灵活性和控制。
  • 例如,如果你有诸如所有单元组件合约集成之类的标记,并且你只想运行组件合约测试,同时排除非功能测试,你可以轻松地配置测试套件,只运行你想要的测试。

过滤:标签

  • 全部(运行所有测试)
  • Cucumber(运行Cucumber测试用例)
  • Junit5(运行所有JUnit单元测试)
  • Mockito(运行Mockito测试用例)
  • WireMock(运行WireMock测试用例)
  • Pact(运行Pact测试用例)
  • Selenium(运行Selenium测试用例)
  • SpringBoot(运行SpringBoot测试用例)
  • 功能(运行功能测试用例)
  • 非功能(运行所有非功能测试用例)
  • 安全(运行安全测试用例)
  • 性能(运行所有性能测试用例)
  • 单元(运行所有JUnit单元测试)
  • 组件(运行所有组件测试用例 — Mockito,Cucumber)
  • 契约(运行所有契约测试用例 — Pact和WireMock)
  • 集成(运行所有集成测试用例 — 使用WireMock)
       <!-- 注释 ===================================== -->  
       <plugin>  
            <groupId>org.apache.maven.plugins</groupId>  
            <artifactId>maven-surefire-plugin</artifactId>  
            <version>${maven-surefire-plugin.version}</version>  
            <configuration>  
                <groups>Unit, Component</groups>  
                <excludedGroups>Pact</excludedGroups>  
                <!-- 用于 Pact 和 JUnit 5 -->  
                <useSystemClassLoader>false</useSystemClassLoader>  
                <systemPropertyVariables>  
                  <pact.rootDir>pacts</pact.rootDir>  
                </systemPropertyVariables>  
             </configuration>  
        </plugin>

自定义标签示例

GitHub Repo: MS 测试快速上手

使用 JUnit 进行系统UnderTest 测试(SUT指系统UnderTest)的最佳实践
  1. 保持测试专注:每个测试方法应验证系统的一个单一行为。
  2. 模拟依赖项:利用像Mockito这样的模拟框架来将系统与其外部依赖项隔离开。
  3. 使用描述性名称:测试方法应当明确地描述正在测试的行为。
  4. 测试边界情况:涵盖边界值测试、无效输入检测和异常场景检查。
  5. 自动化和集成:确保系统的JUnit测试是自动化CI/CD流水线中的一部分。

在我们用 JUnit 5 平台搭建好基础后,来研究 行为驱动开发(BDD) 模式。

行为驱动开发

这是一种敏捷的软件开发方法,强调开发人员、测试人员以及其他非技术人员,如业务分析师之间的合作

它主要通过用户故事和示例来定义系统的行为,这些故事和示例用简单易懂的商业术语编写。

行为驱动开发(BDD)的核心理念是使用Gherkin语法(如_给定-当-那么)来定义验收标准,从而创建系统行为的共同理解。这些标准作为自动化测试的基础,确保系统符合业务期望。

来源:微服务测试策略(点击这里)

BDD的关键特性:

  • 促进各方的合作。
  • 使用Gherkin语言(例如在Cucumber或SpecFlow等工具中使用的一种特定格式)来定义场景。
  • 测试更侧重于用户的实际行为和结果,而不仅仅是实现细节。

来源:详情见:微服务测试方法

来源:该演讲的来源是微服务测试方法

BDD与测试驱动开发(TDD)的不同之处

虽然测试驱动开发(TDD)和行为驱动开发(BDD)都旨在通过测试提高软件质量,但它们的侧重点和方法有所不同。

TDD:首先写测试来指导代码实现。

  • 测试是技术性的,主要关注代码的实现细节。
  • 开发者通常会在实现功能前编写单元测试。

BDD:围绕与利益相关者合作来定义系统行为。

  • 场景主要从用户的角度来描述系统应该怎样做。

TDD 示例(测试驱动开发): 测试是用编程语言如 Java、Python 或 C# 编写。

@Test
void 测试加法() {
    // 测试加法功能,验证2加3等于5
    assertEquals(5, 计算器.add(2, 3));
}

行为驱动开发示例: 场景以易于理解的语言编写(比如,Gherkin)。

    场景:两个数相加  
      给定数字2和3这两个数字  
      将它们相加后  
      结果应为5
利益相关者协作
  • TDD :主要由开发人员驱动的过程,测试以验证技术正确性。
  • BDD :促进开发人员、测试人员和业务方合作,确保所有人都能达成共识。
范围如下:
  • TDD :专注于单元测试级别,确保每个代码单元按预期运行。
  • BDD :在更高层次上运作,涵盖功能或行为,通常与用户故事结合。
一些工具:
  • TDD : JUnit, TestNG, NUnit, PyTest, 等。
  • BDD : Cucumber, SpecFlow, JBehave, Behave, Mockito, RestAssured, 等。
BDD / TDD 简介
  • TDD 是一种以开发为中心的手段,通过从单元测试开始帮助构建更稳健的代码。
  • BDD 是一种以行为为中心的模式,通过定义和测试用户行为来确保系统满足业务需求。

我们将从探讨Mockito在此领域的关键作用开始。在上一次的讨论中,我们观察到Mockito在使用GitHub Copilot自动生成测试用例方面有着显著的贡献。

来源:微服务测试策略(参考此演讲稿)

同时,诸如Cucumber, Rest Assured之类的工具在推动测试自动化和促进有效BDD方法方面起着重要作用。

Google 趋势工具, Mockito

第二部分:Mockito

Mockito 是一个强大且广泛使用的 Java 库,用于创建模拟对象,这些对象在单元测试中对隔离被测系统至关重要。

它的主要目的是模拟依赖项的行为模式,使开发人员能够来独立地测试SUT无需依赖外部系统,如数据库、API或服务

Mockito 使得创建模拟、桩和间谍更简单,允许开发人员设定预期行为、返回值并检查交互

当与JUnit 5集成时,Mockito通过其扩展机制(如@ExtendWith(MockitoExtension.class))提供无缝集成支持,该机制会自动设置用@Mock@Spy注解的模拟对象或间谍对象。

    @TestInstance(TestInstance.Lifecycle.PER_CLASS)  
    @ExtendWith(MockitoExtension.class)  
    class OrderItemGroupTest {  

     @Mock  
     OrderRepository orderRepo;  

     @Mock  
     PaymentService paymentService;  

     @InjectMocks  
     OrderServiceImpl orderService;  
     // ... 仅展示相关代码片段  
    }

// 该翻译仅用于上下文理解,不应在实际中文编程环境中使用,注解和类名应保持英文。

1. @Mock OrderRepository orderRepo;

  • 创建一个用于OrderRepository依赖的模拟对象
  • 模拟对象以可控方式模拟真实OrderRepository的行为,允许你为方法调用定义自定义的响应。

2. @Mock 支付服务 paymentService;

  • 类似于 orderRepo,这为 PaymentService 依赖项创建了一个模拟。

3. @InjectMocks OrderServiceImpl orderService;

  • 将 mocks(orderRepo 和 paymentService)注入到 被测试系统(SUT),也就是 OrderServiceImpl 中。
  • 通过用模拟对象替换其依赖项来确保 SUT 可以独立地进行测试。

Mockito 示例 — 支付状态被接受为有效

     @Test  
     @DisplayName("1. 测试支付已被接受")  
     void testValidatePaymentAccepted() {  
      // 假设订单已经准备好  
      order = OrderMock.createOrder1();  

      // 当  
      when(orderRepo.saveOrder(order))  
       .thenReturn(order);  
      when(paymentService.processPayments(order.getPaymentDetails()))  
       .thenReturn(paymentAccepted);  

      OrderEntity processedOrder = orderService.processOrder(order);  

      // 验证支付状态是否为已接受  
      assertEquals(  
        OrderStatus.PAID,  
        processedOrder.getOrderStatus()  
        );  
     }

Mockito 示例:支付状态被拒

     @Test  
     @DisplayName("2. 测试支付失败")  
     void testValidatePaymentDeclined() {  
      // 给定订单已准备就绪  
      order = OrderMock.createOrder1();  

      // 当  
      when(orderRepo.saveOrder(order))  
       .thenReturn(order);  
      when(paymentService.processPayments(order.getPaymentDetails()))  
       .thenReturn(paymentDeclined);  

      OrderEntity processedOrder = orderService.processOrder(order);  

      // 然后确认支付状态为失败  
      assertEquals(  
        OrderStatus.PAYMENT_DECLINED,  
        processedOrder.getOrderStatus()  
        );  
     }

这种集成实现了更清洁的测试代码,减少了重复代码,并确保了测试框架与模拟功能的实现之间的更紧密的结合,是编写高效且易于维护的单元测试不可或缺的工具。

此设置的主要好处
  • 可控环境:通过使用模拟注解(@Mock),你可以将被测试对象(OrderServiceImpl)与真实依赖隔离开,专注于其行为表现。
  • 灵活的依赖模拟测试:你可以通过模拟对象定义方法调用的具体行为,使得测试不同的场景(如成功、失败或异常情况)变得更加容易。
  • 增强的测试组织与执行@TestMethodOrder 确保测试方法执行的可预测性和可重复性,使测试更加有序。
  • 简化了模拟对象的管理@ExtendWith(MockitoExtension.class) 自动初始化模拟对象,减少了样板代码。
示例用例

这种设置非常适合用来测试OrderServiceImpl类的业务逻辑,例如:

  • 可以验证下单时,orderRepo.saveOrder() 方法会被调用。
  • 通过让 paymentService.processPayments() 返回无效数据来模拟支付失败的情况,并测试 OrderServiceImpl 如何处理这种情况。
     @DisplayName("3. 处理订单并进行验证")
     @Order(3)
     @Test
     void testPlaceOrder() {
      // 给定订单已准备好:订单在 setup() 方法中设置
      // 然后
      when(orderRepo.saveOrder(order)).thenReturn(order);
      when(paymentService.processPayments(order.getPaymentDetails()))
        .thenReturn(paymentAccepted);
      // 然后
      orderService.processOrder(order);

      // 验证
      verify(orderRepo).saveOrder(order); // 确保订单已被保存
      verify(paymentService).processPayments(any()); // 确保支付已被处理
     }

在第二部分中,我们探讨了如何使用GitHub Copilot高效地生成测试用例。

理解 JUnit 5 等框架更深入,并结合 Hamcrest、Google Truth、AssertJ 等断言库,以及配合 Mockito 使用,能帮你为组件测试编写更强大和可靠的测试用例。

这种知识也提高了使用GitHub Copilot自动生成的测试用例的质量。在第四部分中,我们将进一步探讨Mockito并改进提示工程技术,以生成更高级和更复杂的测试用例,与Copilot一起工作。

大家好,祝大家有一个美好的假期!圣诞快乐!🎄

来源:Slidechef — 2025年非常愉快的新年

GitHub仓库: MS Test Quick Start: Spring Boot Test 3.3.4、JUnit 5、TestNG 7、Spock 3、DiffBlue Cover、RestAssured 5、Cucumber 6、Selenium 4、Mockito 3、WireMock 3、Pact 4用于微服务应用自动化测试示例。所有这些测试框架都必须用于微服务应用的全面自动化测试。

更多详细信息请参阅我的SpeakerDeck.Com 演讲稿:AI驱动的测试自动化策略。

Java 23 系列教程
Java 23 版本, Spring Boot 3.3.4: AI 驅動的測試生成系列文章
  1. Java 23, SpringBoot 3.3.4: AI 驱动的测试生成 — 第 1 部分
  2. Java 23, SpringBoot 3.3.4: AI 驱动的 GitHub Copilot — 第 2 部分
  3. Java 23, SpringBoot 3.3.4: AI 驱动的 JUnit 5, Mockito — 第三部分
  4. Java 23, SpringBoot 3.3.4: AI 驱动的 Mockito, TestNG — 第四部分(即将发布)
  5. Java 23, SpringBoot 3.3.4: AI 驱动的 Cucumber, Selenium — 第五部分(即将发布)
  6. Java 23, SpringBoot 3.3.4: AI 驱动的 WireMock — 第六部分(即将发布)
  7. Java 23, SpringBoot 3.3.4: AI 驱动的 Pact — 第七部分(即将发布)
更多的研究
  1. JUnit 5: JUnit 5 平台框架
  2. Mockito: Mockito 框架
  3. Hamcrest: Hamcrest 断言库
  4. Google Truth: Truth 断言库(Java & Android)
  5. AssertJ: 概览
点击查看更多内容
TA 点赞

若觉得本文不错,就分享一下吧!

评论

作者其他优质文章

正在加载中
  • 推荐
  • 评论
  • 收藏
  • 共同学习,写下你的评论
感谢您的支持,我会继续努力的~
扫码打赏,你说多少就多少
赞赏金额会直接到老师账户
支付方式
打开微信扫一扫,即可进行扫码打赏哦
今天注册有机会得

100积分直接送

付费专栏免费学

大额优惠券免费领

立即参与 放弃机会
微信客服

购课补贴
联系客服咨询优惠详情

帮助反馈 APP下载

慕课网APP
您的移动学习伙伴

公众号

扫描二维码
关注慕课网微信公众号

举报

0/150
提交
取消