前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >Junit5参数化实战,让测试更优雅

Junit5参数化实战,让测试更优雅

作者头像
测试蔡坨坨
发布2023-09-11 14:30:06
4411
发布2023-09-11 14:30:06
举报

前言

你好,我是测试蔡坨坨。

在代码的世界里,有一片自动化的花园,那里的用例是微风吹拂下的花朵,绽放着不同的颜色。在这片花园中,我们常常遇到一个美妙的情景:相同的测试流程,却需要随着业务的风向,切换不同的测试数据。这就像是一支曲子,相同的旋律,却因音符的不同而显得迥然不同。

就如诗人所言,方法的舞步相同,只是入参的音符不同。我们需要思考等价的类别,探寻边界的价值,从而谱写出一曲动人心弦的测试乐章。

然而,如果把所有的测试数据都堆砌在方法中,就像是在花园里撒下过多的种子,反而显得杂乱无章。那用例的可维护性和可阅读性,就如同被昏暗的雾霭遮掩了一般。

而在这个代码的诗坛上,各路诗人都创造了解决方案的花园。就如音乐家有琴键与弦线,TestNG 有 @Parameters 和 @DataProvider,Pytest 也拥有 @pytest.mark.parametrize 等乐符,为我们奏响了测试的乐章。

当然,Junit也为我们提供了一套卓越的解决方案,让参数化用例的编写变得更加优雅。这项特性使得我们能够以一种优美的方式,运行单个测试多次,每次运行仅仅参数有所不同。更妙的是,每条测试用例都能够独立存在,彼此之间毫不干扰。

在这篇文章中,我将带领大家深入体验一下Junit5是如何实现参数化的奇妙之处。让我们一同踏上这段探索之旅,领略代码世界的多彩风景。

Junit5 参数化

Junit5参数化的魅力令人为之倾倒,其使用之便捷简直令人惊叹。只需嵌入少许注解,便能开启一场多维数据之旅,而数据的来源更是多姿多彩:单参数、多参数、甚至文件中的数据、方法所提供的数据,无一不在其考虑之列。这一巧妙设计,为测试带来了前所未有的灵活性与丰富性。

官方文档:https://junit.org/junit5/docs/current/user-guide/#writing-tests-parameterized-tests

安装依赖

欲使用Junit5的参数化,需要在Junit Platform的基础上导入junit-jupiter-params依赖包。

如果你的项目是使用Maven构建,那么只需要在pom文件中引入以下依赖即可:

代码语言:javascript
复制
<!-- https://mvnrepository.com/artifact/org.junit.jupiter/junit-jupiter-params -->
<dependency>
    <groupId>org.junit.jupiter</groupId>
    <artifactId>junit-jupiter-params</artifactId>
    <version>5.10.0</version>
    <scope>test</scope>
</dependency>
单参数 @ValueSource

@ValueSource 是最简单的参数化方式,它允许往测试方法中传递一个数据或者迭代器。

支持以下类型的单参数数据的参数化:

参数

参数类型

shorts

short

bytes

byte

ints

int

longs

long

floats

float

doubles

double

chars

char

booleans

boolean

strings

java.long.String

classes

java.long.Class

使用步骤
  • 使用参数化用例时,需将@Test注解换成@ParameterizedTest
  • 添加单参数化注解@ValueSource
  • 注意:如果@Test和@ParameterizedTest同时使用,则会多执行一次,且由于@Test无法传递参数,所以运行时会报ParameterResolutionException异常
实战演练

为方便演示,下面将使用一道算法题实现的功能作为被测对象,进行参数化用例的实战演练:

代码语言:javascript
复制
package top.caituotuo.demo;

import java.util.HashMap;
import java.util.Map;

/**
 * author: 测试蔡坨坨
 * datetime: 2023-8-21 02:12:43
 * function: 给定一个字符串,找出不含有重复字符的最长子串的长度。
 * 例如:
 * 给定 "abcabcbb" ,没有重复字符的最长子串是 "abc" ,长度为 3。
 * 给定 "bbbbb" ,最长的子串就是 "b" ,长度是 1。
 * 给定 "pwwkew" ,最长的子串是 "wke" ,长度是 3。
 * 请注意答案必须是一个子串,"pwke" 是子序列 而不是子串。
 */

public class DemoTest {
    public int lengthOfLongestSubstr(String s) {
        if (s == null || s.length() == 0) {
            return 0;
        }
        // 定义dp数组的含义:以字符s[i]结尾时,不重复的子串长度为dp[i]
        int[] dp = new int[s.length()];
        // 初始值
        dp[0] = 1;
        Map<Character, Integer> map = new HashMap<>();
        map.put(s.charAt(0), 0);
        int maxLen = 1;
        int startIndex = 0;
        // 状态转换关系式
        for (int i = 1; i < s.length(); i++) {
            if (!map.containsKey(s.charAt(i))) {
                dp[i] = dp[i - 1] + 1;
            } else {
                int k = map.get(s.charAt(i));
                dp[i] = i - k <= dp[i - 1] ? i - k : dp[i - 1] + 1;
            }
            // maxLen = Math.max(maxLen, dp[i]);
            if (dp[i] > maxLen) {
                maxLen = dp[i];
                startIndex = i - maxLen + 1;
            }
            map.put(s.charAt(i), i);
        }
        System.out.printf("原字符串:%s,最长不重复子串:%s,长度:%s%n", s, s.substring(startIndex, startIndex + maxLen), maxLen);
        return maxLen;
    }
}

以String类型的单参数化举栗:

代码语言:javascript
复制
/**
 * @param s 测试方法中声明形参,代表参数化通过这个形参给到测试方法去使用
 */
// @Test
// 将@Test注解换成@ParameterizedTest注解,指明参数化测试用例
@ParameterizedTest
// 单参数注解,示例中为String类型参数化
@ValueSource(strings = {"abcabcbb", "pwwkew"})
public void test(String s) {
    assertEquals(3, new DemoTest().lengthOfLongestSubstr(s));
}

运行结果:

从Junit5.4开始,可以使用@NullSource、@EmptySource和@NullAndEmptySource注解分别将单个null值、单个Empty值 和 null+Empty 作为参数传递给测试方法,如下示例:

代码语言:javascript
复制
@ParameterizedTest
@NullSource
@EmptySource
public void test0(String s) {
    assertEquals(0, new DemoTest().lengthOfLongestSubstr(s));
}

运行结果:

代码语言:javascript
复制
@ParameterizedTest
@NullAndEmptySource
public void test1(String s) {
    assertEquals(0, new DemoTest().lengthOfLongestSubstr(s));
}

运行结果:

多参数 @CsvSource

在诸多场景中,单一参数恐难以尽善尽美,往往需同时传入测试数据预期结果来验证测试逻辑是否符合预期。为此,多参数的参数化方式将至关重要。

还是用前面所说的算法题举栗,有以下两条用例:

  1. 给定 "abcabcbb" ,没有重复字符的最长子串是 "abc" ,长度为 3。
  2. 给定 "bbbbb" ,没有重复字符的最长子串是 "b" ,长度为 1。
使用步骤
  • 添加多参数参数化注解 @CsvSource
  • @CsvSource 通过默认或指定的分隔符实现参数化
实战演练
默认分隔符
代码语言:javascript
复制
@ParameterizedTest
// 传递的参数格式是一个集合,如果是多个参数,使用默认分隔符","分开
@CsvSource({"abcabcbb,3", "bbbbb,1"})
public void test2(String s, int n) {
    assertEquals(n, new DemoTest().lengthOfLongestSubstr(s));
}

运行结果:

指定分隔符

@CsvSource 的分隔符默认是逗号,在实际测试中,若逗号需要被当做参数进行传递,那么我们还可以使用delimiterString属性来自定义分割符号,如下示例:

代码语言:javascript
复制
@ParameterizedTest
// 使用delimiterString指定分隔符,使用value指定数据源
@CsvSource(value = {"abcabcbb|3", "bbbbb|1"}, delimiterString = "|")
public void test3(String s, int n) {
    assertEquals(n, new DemoTest().lengthOfLongestSubstr(s));
}

运行结果:

代码语言:javascript
复制
@ParameterizedTest
// 指定分隔符为“测试蔡坨坨”
@CsvSource(value = {"abcabcbb测试蔡坨坨3", "bbbbb测试蔡坨坨1"}, delimiterString = "测试蔡坨坨")
public void test4(String s, int n) {
    assertEquals(n, new DemoTest().lengthOfLongestSubstr(s));
}

运行结果:

多参数文件参数化 @CsvFileSource

实际测试中,CSV测试数据常存储在CSV文件之中,需要通过读取文件来获取测试数据。

此时就可以使用@CsvFileSource注解来指定文件路径,实现文件数据源的读取。

使用步骤
  • 添加多参数文件参数化注解 @CsvFileSource
  • 在项目的 test/resources 中新增测试数据 csv 文件
  • @CsvFileSource 支持指定分隔符进行参数化
实战演练

通常情况下,@CsvFileSource注解会去解析每一行,但有些时候第一行可能是列名,因此我们可以添加numLinesToSkip = 1属性来跳过第1行。同样与@CsvSource一样,也可以使用delimiterString来指定分隔符。

测试数据:

代码语言:javascript
复制
@ParameterizedTest
@CsvFileSource(resources = "/data.csv", numLinesToSkip = 1, delimiterString = "-")
public void test5(String s, int n) {
    assertEquals(n, new DemoTest().lengthOfLongestSubstr(s));
}

运行结果:

方法参数 @MethodSource

有时参数的来源颇非简单的数据结构,参数存储的文件也不一定是CSV文件,或者还有Excel、YAML等。

于是,这些错综复杂的数据结构欲化身为测试参数,需借助一些巧妙之法,将其读取转换为方法,并将方法作为参数传递给测试方法。

Junit5同样提供了妙不可言的解决方案,我们可以借助@MethodSource注解,传递复杂的迭代对象到测试方法中。@MethodSource使用非常灵活,既能从文件中提取,亦能从接口的返回值中提取。毕竟,其本质是以一个方法作为参数的来源,那么任何复杂的数据结构我们都可以在方法中做定制化处理。

使用步骤
  • 通过@MethodSource注解引用方法作为参数化的数据源信息,允许引用一个或多个测试类的工厂方法,这样的方法必须返回一个Stream,Iterable,Iterator或参数数组。另外,这种方法不能接受任何参数。
  • 在@MethodSource注解的参数必须是静态的工厂方法,除非测试类被注释为@TestInstance(Lifecycle.PER_CLASS)
  • 静态工厂方法的返回值需要和测试方法的参数对应
  • 如果在@MethodSource注解中未指明方法名,会自动调用与测试方法同名的静态方法
实战演练

如果只需要一个参数,则可以返回参数类型的实例Stream,如下示例:

代码语言:javascript
复制
package top.caituotuo.demo;

import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.MethodSource;

import java.util.stream.Stream;

/**
 * author: 测试蔡坨坨
 * datetime: 2023/8/26 16:38
 * function: 单参数方法
 */
public class MethodSourceTest {
    /**
     * @param name 添加形参,形参的类型要和静态方法内部的元素类型一致
     */
    @ParameterizedTest
    // 通过@MethodSource注解指定数据源的方法名
    @MethodSource("stringProvider")
    void test1(String name) {
        System.out.println(name);
    }

    /**
     * 定义一个静态方法,提供参数化数据源
     *
     * @return 返回Stream流
     */
    static Stream<String> stringProvider() {
        return Stream.of("测试蔡坨坨", "小趴蔡", "IT小学生蔡坨坨");
    }
}

运行结果:

支持原始类型(DoubleStreamIntStreamLongStream)的流,示例如下:

代码语言:javascript
复制
@ParameterizedTest
@MethodSource("range")
void testWithRangeMethodSource(int argument) {
    assertNotEquals(9, argument);
}
static IntStream range() {
    return IntStream.range(0, 20).skip(10);
}

如果测试方法声明多个参数,则需要返回一个集合或Arguments实例流,如下所示:

代码语言:javascript
复制
package top.caituotuo.demo;

import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.Arguments;
import org.junit.jupiter.params.provider.MethodSource;

import java.util.Arrays;
import java.util.List;
import java.util.stream.IntStream;
import java.util.stream.Stream;

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

/**
 * author: 测试蔡坨坨
 * datetime: 2023/8/22 23:32
 * function: 给出一个包含3个数字的列表,判断此3个数字能否组成三角形。能的话输出true,不能的话输出false。
 * 输入[3,4,3] 输出true
 * 输入[1,2,3] 输出false
 */
public class DemoTest {
    public boolean isTriangle(List<Integer> sides) {
        if (sides.size() != 3) {
            return false;
        }

        return sides.stream().allMatch(side -> side > 0) &&
                IntStream.range(0, 3).allMatch(i ->
                        sides.get(i) + sides.get((i + 1) % 3) > sides.get((i + 2) % 3));
    }

    @ParameterizedTest
    @MethodSource({"getTestSides"})
    public void test(List<Integer> sides, boolean expectedResult) {
        assertEquals(expectedResult, new DemoTest().isTriangle(sides));
    }

    static Stream<Arguments> getTestSides() {
        return Stream.of(
                Arguments.of(Arrays.asList(3, 4, 3), true),
                Arguments.of(Arrays.asList(1, 2, 3), false),
                Arguments.of(Arrays.asList(-3, 4, 5), false),
                Arguments.of(Arrays.asList(3, 4, 5, 6), false)
        );
    }
}

运行结果:

关于 Junit5 参数化的探讨,暂时就聊到这里,我们将在下一期再度相聚。期待与你再次分享更多关于优雅测试的心灵之旅。愿我们的交流如同代码一般,不断升华。再会。

以上,完。

本文参与 腾讯云自媒体同步曝光计划,分享自微信公众号。
原始发表:2023-08-26,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 测试蔡坨坨 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • Junit5 参数化
    • 安装依赖
      • 单参数 @ValueSource
        • 使用步骤
        • 实战演练
      • 多参数 @CsvSource
        • 使用步骤
        • 实战演练
      • 多参数文件参数化 @CsvFileSource
        • 使用步骤
        • 实战演练
      • 方法参数 @MethodSource
        • 使用步骤
        • 实战演练
    相关产品与服务
    腾讯云服务器利旧
    云服务器(Cloud Virtual Machine,CVM)提供安全可靠的弹性计算服务。 您可以实时扩展或缩减计算资源,适应变化的业务需求,并只需按实际使用的资源计费。使用 CVM 可以极大降低您的软硬件采购成本,简化 IT 运维工作。
    领券
    问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档