junit的测试方法是(排列和产品进行彻底的)
junit的测试方法是(排列和产品进行彻底的)X 的组合是:在我们继续之前,我们从数学的角度提醒自己什么是组合、排列和乘积。假设我们有一个集合 X = { A,B,C } ;这个库提供了编写 JUnit 测试的支持类。它同时支持 JUnit4和 JUnit5。可以利用排列、组合和产品等工具来创建详尽的测试。Agitar One 可以自动识别和生成测试,而 Chronicle Test Framework 为编写自己的测试提供了编程支持。在本文中,我们将讨论Chronicle测试框架。假设我们已经编写了一个 java.util 的自定义实现。名为 MyList 的列表,并且我们希望针对参考实现(如 ArrayList)测试该实现。进一步,假设我们有许多操作 O1,O2,... ,On。我们可以应用到这些对象上。现在的问题是: 我们如何编写测试来确保两个对象提供相同的结果,而不管操作是如何应用的?
单元测试是提供高质量软件过程中不可或缺的一部分。但是,如何编写涵盖多个操作的所有变体的测试呢?阅读本文,了解如何将 JUnit5与组合、排列和产品结合使用
测试支持库
有许多库可以在不同方面改进测试,下面是其中的一些:
Agitar OneAgitator自动创建动态测试用例,合成输入数据集,并分析结果。
Chaos Monkey混沌猴负责随机终止生产中的实例,以确保工程师实现他们的服务能够对实例故障具有弹性。
Chronicle Test Framework这个库提供了编写 JUnit 测试的支持类。它同时支持 JUnit4和 JUnit5。可以利用排列、组合和产品等工具来创建详尽的测试。Agitar One 可以自动识别和生成测试,而 Chronicle Test Framework 为编写自己的测试提供了编程支持。
在本文中,我们将讨论Chronicle测试框架。
目的假设我们已经编写了一个 java.util 的自定义实现。名为 MyList 的列表,并且我们希望针对参考实现(如 ArrayList)测试该实现。进一步,假设我们有许多操作 O1,O2,... ,On。我们可以应用到这些对象上。现在的问题是: 我们如何编写测试来确保两个对象提供相同的结果,而不管操作是如何应用的?
组合、排列和产品在我们继续之前,我们从数学的角度提醒自己什么是组合、排列和乘积。假设我们有一个集合 X = { A,B,C } ;
X 的组合是:
[]
[A]
[B]
[C]
[A B]
[A C]
[B C]
[A B C]
X 的排列是:
[A B C]
[A C B]
[B A C]
[B C A]
[C A B]
[C B A]
X 的所有组合的排列是:
[]
[A]
[B]
[C]
[A B]
[B A]
[A C]
[C A]
[B C]
[C B]
[A B C]
其中[]表示无元素序列。从上面的序列可以看出,随着集合成员数量的增加,变体的数量将迅速增加。这对可以编写的测试的详尽性提供了一个实际的限制。
另一个概念(本文没有举例说明)是“产品”(又名笛卡尔产品)。
假设一个序列 s1 = [ A,B ]是一种类型,另一个序列 s2 = [1 2]是另一种类型,
s1和 s2的乘积是:
[A 1]
[A 2]
[B 1]
[B 2]
产品与数据库“连接”操作有许多相似之处,并且与嵌套循环相关。
测试框架在这个测试中,我们将使用支持上述组合、排列和产品特性的开源 Chronicle-Test-Framework。这里有一个例子:
Java
/ Prints: [] [A] [B] [C] [A B] [B C] … (8 sequences)
Combination.of("A" "B" "C")
.forEach(System.out::println)
这将打印{ A,B,C }的所有组合,其结果与前一章中的结果相同。以同样的方式,下面的示例将打印出{ A,B,C }的所有排列:
Java
// Prints: [A B C] [A C B] [B A C] … (6 sequences)
Permutation.of("A" "B" "C")
.forEach(System.out::println)
在下面的例子中,我们结合了组合和排列的能力:
Java
// Prints: [] [A] [B] [C] [A B] [B A] …(16 sequences)
Combination.of("A" "B" "C")
.flatMap(Permutation::of)
.forEach(System.out::println)
上面的方法产生了一个 Stream of Collection 元素,可以很容易地适应其他框架,比如 JUnit5。
说到产品,事情的运作方式略有不同。下面是一个例子:
Java
final List<String> strings = Arrays.asList("A" "B");
final List<Integer> integers = Arrays.asList(1 2);
Product.of(strings integers)
.forEach(System.out::println);
这将打印:
Product2Impl{first=A second=1}
Product2Impl{first=A second=2}
Product2Impl{first=B second=1}
Product2Impl{first=B second=2}
如果使用较新的 Java 版本,通常会使用以下方案:
Java
record StringInteger(String string Integer integer){}
Product.of(strings integers StringInteger::new)
.forEach(System.out::println);
这将产生:
StringInteger[string=A integer=1]
StringInteger[string=A integer=2]
StringInteger[string=B integer=1]
StringInteger[string=B integer=2]
在我看来,上面的代码比默认的 Product2Impl tuple 要好,因为记录是一个“名义元组”,其中状态元素的名称和类型在记录头中声明,而 Product2Impl 依赖于泛型类型,并且“ first”和“ second”作为名称。
目标假设我们要确保两个 java.lang。列表实现对于许多变异操作的行为是相同的。在这里,列表包含整数值(即实现列表 < 整数 >)。
我们首先确定要使用的变异操作:
- list.clear()
- list.add(1)
- list.remove((Integer) 1) //将删除对象1而不是@index 1
- list.addAll(Arrays.asList(2 3 4 5))
- list.removeIf(ODD)
Where Predicate<Integer> ODD = v -> v % 2 == 1.
在本文中,我们将首先使用 ArrayList 和 LinkedList 进行比较。
正如读者可能怀疑的那样,List 类型将通过所有可能的操作序列彼此进行测试。但是,如何才能做到这一点呢?
解决方案JUnit5提供了@TestFactory 注释和 DynamicTest 对象,允许测试工厂返回 DynamicTest 实例流。这是我们可以利用的东西,如下所示:
Java
private static final Collection<NamedConsumer<List<Integer>>> OPERATIONS =
Arrays.asList(
NamedConsumer.of(List::clear "clear()")
NamedConsumer.of(list -> list.add(1) "add(1)")
NamedConsumer.of(list -> list.remove((Integer) 1) "remove(1)")
NamedConsumer.of(list -> list.addAll(Arrays.asList(2 3 4 5)) "addAll(2 3 4 5)") br NamedConsumer.of(list -> list.removeIf(ODD) "removeIf(ODD)")br);brbrbrbr@TestFactorybrStream<DynamicTest> validate() {br return DynamicTest.stream(Combination.of(OPERATIONS)br .flatMap(Permutation::of) br Object::toString br operations -> {br List<Integer> first = new ArrayList<>();br List<Integer> second = new LinkedList<>();br operations.forEach(op -> {br op.accept(first);br op.accept(second);br });br assertEquals(first second);br });br}
这是整个解决方案,当在 IntelliJ (或其他类似工具)下运行时,将执行以下测试(为简洁起见,只显示了326个测试中的前16个) :
图1显示了326个测试的前16个测试运行。
NamedConumer 用于标准 java.util.function 的原因。消费者是,它允许显示实名,而不是像 ListDemoTest $$Lambda $350/0x000000800ca9a88@3d8314f0这样的神秘的 Lambda 引用。它在输出中当然看起来更好,并且提供了更好的调试功能。
拓展概念假设我们有更多的 List 实现需要测试,比如:
- ArrayList
- LinkedList
- CopyOnWriteArrayList
- Stack
- Vector
为了扩展这个概念,我们首先创建一个 List < Integer > 构造函数的集合:
Java
private static final Collection<Supplier<List<Integer>>> CONSTRUCTORS =
Arrays.asList(
ArrayList::new
LinkedList::new
CopyOnWriteArrayList::new
Stack::new
Vector::new);
使用构造函数而不是实例的原因是,我们需要能够为每个动态测试创建新的 List 实现。
现在,我们只需要对之前的测试做一些小的修改:
@TestFactorybrStream<DynamicTest> validateMany() {
return DynamicTest.stream(Combination.of(OPERATIONS)
.flatMap(Permutation::of)
Object::toString
operations -> {
// Create a fresh list of List implementationsbr List<List<Integer>> lists = CONSTRUCTORS.stream()br .map(Supplier::get)
严格地说,这有点作弊,因为一个单一的动态测试包含多个 List 对的几个子测试。然而,在单独的动态测试下,在各种断言中将上面的代码修改为 latMap ()是相当容易的。这留给读者作为练习。
最后警告组合、排列和产品是有效的工具,可以增加测试覆盖率,但是它们也可以增加运行所有测试的时间,因为序列的数量会爆炸,即使输入变体的数量只有适度的增加。
例如,下面列出了一些给定的大小为 s 的集合 S 的所有组合的排列数:
s = size(S) |
poc(s) (permutations of combinations) |
0 |
1 |
1 |
2 |
2 |
5 |
3 |
16 |
4 |
65 |
5 |
326 |
6 |
1 957 1957 |
7 |
13 700 13700 |
8 |
109 601 |
9 |
986 410 |
10 |
986 4101 |
11 |
108 505 112 |
表1显示了某些集合大小的组合排列数。
更一般地说,可以表明: poc (s) = poc (s-1) * s 1。
如果有疑问,可以使用。Count ()流操作,该操作将返回返回的序列数,如下所示:
long poc6 = Combination.of(1 2 3 4 5 6)
.flatMap(Permutation::of)
.count();
毫不奇怪,上面的流将返回1 957,因为有6个输入元素。
组合数学可以产生巨大的影响。但是,一定要控制动态测试的数量,否则测试时间可能会显著增加。
类似于历史记录队列和历史记录映射的库利用历史记录测试框架来改进测试。下面是 Chronicle Map 的一个例子,其中5个 Map 操作被彻底测试,仅用几行代码就提供了326个动态测试。
资源Open-Source Chronicle-Test-Framework
https://github.com/OpenHFT/Chronicle-Test-Framework
The code in this article (Open-Source)
https://github.com/OpenHFT/Chronicle-Test-Framework/blob/develop/src/test/java/net/openhft/chronicle/testframework/internal/ListDemoTest.java