JunitJunit5参数化实战,让测试更优雅
蔡坨坨转载请注明出处❤️
作者:测试蔡坨坨
原文链接:caituotuo.top/b0dfc061.html
前言
你好,我是测试蔡坨坨。
在代码的世界里,有一片自动化的花园,那里的用例是微风吹拂下的花朵,绽放着不同的颜色。在这片花园中,我们常常遇到一个美妙的情景:相同的测试流程,却需要随着业务的风向,切换不同的测试数据。这就像是一支曲子,相同的旋律,却因音符的不同而显得迥然不同。
就如诗人所言,方法的舞步相同,只是入参的音符不同。我们需要思考等价的类别,探寻边界的价值,从而谱写出一曲动人心弦的测试乐章。
然而,如果把所有的测试数据都堆砌在方法中,就像是在花园里撒下过多的种子,反而显得杂乱无章。那用例的可维护性和可阅读性,就如同被昏暗的雾霭遮掩了一般。
而在这个代码的诗坛上,各路诗人都创造了解决方案的花园。就如音乐家有琴键与弦线,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文件中引入以下依赖即可:
1 2 3 4 5 6 7
| <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 |
使用步骤
实战演练
为方便演示,下面将使用一道算法题实现的功能作为被测对象,进行参数化用例的实战演练:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48
| package top.caituotuo.demo;
import java.util.HashMap; import java.util.Map;
public class DemoTest { public int lengthOfLongestSubstr(String s) { if (s == null || s.length() == 0) { return 0; } 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; } 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类型的单参数化举栗:
1 2 3 4 5 6 7 8 9 10 11
|
@ParameterizedTest
@ValueSource(strings = {"abcabcbb", "pwwkew"}) public void test(String s) { assertEquals(3, new DemoTest().lengthOfLongestSubstr(s)); }
|
运行结果:
从Junit5.4开始,可以使用@NullSource、@EmptySource和@NullAndEmptySource注解分别将单个null值、单个Empty值 和 null+Empty 作为参数传递给测试方法,如下示例:
1 2 3 4 5 6
| @ParameterizedTest @NullSource @EmptySource public void test0(String s) { assertEquals(0, new DemoTest().lengthOfLongestSubstr(s)); }
|
运行结果:
1 2 3 4 5
| @ParameterizedTest @NullAndEmptySource public void test1(String s) { assertEquals(0, new DemoTest().lengthOfLongestSubstr(s)); }
|
运行结果:
多参数 @CsvSource
在诸多场景中,单一参数恐难以尽善尽美,往往需同时传入测试数据
和预期结果
来验证测试逻辑是否符合预期。为此,多参数的参数化方式将至关重要。
还是用前面所说的算法题举栗,有以下两条用例:
- 给定 “abcabcbb” ,没有重复字符的最长子串是 “abc” ,长度为 3。
- 给定 “bbbbb” ,没有重复字符的最长子串是 “b” ,长度为 1。
使用步骤
- 添加多参数参数化注解 @CsvSource
- @CsvSource 通过默认或指定的分隔符实现参数化
实战演练
默认分隔符
1 2 3 4 5 6
| @ParameterizedTest
@CsvSource({"abcabcbb,3", "bbbbb,1"}) public void test2(String s, int n) { assertEquals(n, new DemoTest().lengthOfLongestSubstr(s)); }
|
运行结果:
指定分隔符
@CsvSource 的分隔符默认是逗号,在实际测试中,若逗号需要被当做参数进行传递,那么我们还可以使用delimiterString属性来自定义分割符号,如下示例:
1 2 3 4 5 6
| @ParameterizedTest
@CsvSource(value = {"abcabcbb|3", "bbbbb|1"}, delimiterString = "|") public void test3(String s, int n) { assertEquals(n, new DemoTest().lengthOfLongestSubstr(s)); }
|
运行结果:
1 2 3 4 5 6
| @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来指定分隔符。
测试数据:
1 2 3 4 5
| @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,如下示例:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32
| package top.caituotuo.demo;
import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.MethodSource;
import java.util.stream.Stream;
public class MethodSourceTest {
@ParameterizedTest @MethodSource("stringProvider") void test1(String name) { System.out.println(name); }
static Stream<String> stringProvider() { return Stream.of("测试蔡坨坨", "小趴蔡", "IT小学生蔡坨坨"); } }
|
运行结果:
支持原始类型(DoubleStream
,IntStream
和LongStream
)的流,示例如下:
1 2 3 4 5 6 7 8
| @ParameterizedTest @MethodSource("range") void testWithRangeMethodSource(int argument) { assertNotEquals(9, argument); } static IntStream range() { return IntStream.range(0, 20).skip(10); }
|
如果测试方法声明多个参数,则需要返回一个集合或Arguments实例流,如下所示:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46
| 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;
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 参数化的探讨,暂时就聊到这里,我们将在下一期再度相聚。期待与你再次分享更多关于优雅测试的心灵之旅。愿我们的交流如同代码一般,不断升华。再会。