基于属性的测试用于 Java 中的自定义有序列表

huangapple go评论124阅读模式
英文:

Property based testing for a custom ordered list in Java

问题

以下是翻译好的内容:

给定以下排序要求:

所有以 "foo" 开头的字符串应该排在前面。

所有以 "bar" 开头的字符串应该排在最后。

不以 "foo" 或 "bar" 开头的字符串也可以出现在列表中。

如何使用属性基础测试来测试实现上述要求的方法,而不会让人头疼?

有没有比以下代码更优雅的方法:

List<String> strings = Arrays.asList("foo", "bar", "bar1", "jar");
Collections.shuffle(strings);
assertListStartWith(strings, "foo");
assertListEndsWith(strings, "bar", "bar1");
assertThat(strings, hasItem("jar"));
英文:

Given the following ordering requirement:

All strings starting with "foo" should be first.

All string starting with "bar" should be last.

Strings that do not start with "foo" or "bar" can also be present in the list.

How can one use Property-Based Testing to test an implementation of the above requirements without getting a headache?

Is there some thing more elegant then the following:

List&lt;String&gt; strings = Arrays.asList(&quot;foo&quot;, &quot;bar&quot;, &quot;bar1&quot;, &quot;jar&quot;);
Collections.shuffle(strings);
assertListStartWith(strings, &quot;foo&quot;);
assertListEndsWith(strings, &quot;bar&quot;, &quot;bar1&quot;);
assertThat(strings, hasItem( &quot;jar&quot;));

答案1

得分: 3

我假设你有一个带有以下签名的排序函数:

List<String> sortFooBar(List<String> list)

我至少可以看到 sortFooBar(list) 应该满足以下五个属性:

  1. 保留所有项目,并且仅保留这些项目在列表中。
  2. 第一个 "foo" 前没有任何其他项目。
  3. 第一个和最后一个 "foo" 之间没有其他项目。
  4. 最后一个 "bar" 后没有任何其他项目。
  5. 第一个和最后一个 "bar" 之间没有其他项目。

在一个真正的函数式编程语言中,这些属性很容易在 Java 中需要一些代码来表达。所以下面是我对这个问题的解决方案,使用 jqwik 作为属性驱动测试框架,以及 AssertJ 进行断言:

import java.util.*;
import java.util.function.*;
import org.assertj.core.api.*;
import net.jqwik.api.*;

class MySorterProperties {
    
    @Property
    void allItemsAreKept(@ForAll List<@From("withFooBars") String> list) {
        List<String> sorted = MySorter.sortFooBar(list);
        Assertions.assertThat(sorted).containsExactlyInAnyOrderElementsOf(list);
    }
    
    @Property
    void noItemBeforeFoo(@ForAll List<@From("withFooBars") String> list) {
        List<String> sorted = MySorter.sortFooBar(list);
        int firstFoo = findFirst(sorted, item -> item.startsWith("foo"));
        if (firstFoo < 0) return;
        Assertions.assertThat(sorted.stream().limit(firstFoo)).isEmpty();
    }
    
    @Property
    void noItemBetweenFoos(@ForAll List<@From("withFooBars") String> list) {
        List<String> sorted = MySorter.sortFooBar(list);
        int firstFoo = findFirst(sorted, item -> item.startsWith("foo"));
        int lastFoo = findLast(sorted, item -> item.startsWith("foo"));
        if (firstFoo < 0 && lastFoo < 0) return;
        List<String> allFoos = sorted.subList(
            Math.max(firstFoo, 0),
            lastFoo >= 0 ? lastFoo + 1 : sorted.size()
        );
        Assertions.assertThat(allFoos).allMatch(item -> item.startsWith("foo"));
    }
    
    @Property
    void noItemAfterBar(@ForAll List<@From("withFooBars") String> list) {
        List<String> sorted = MySorter.sortFooBar(list);
        int lastBar = findLast(sorted, item -> item.startsWith("bar"));
        if (lastBar < 0) return;
        Assertions.assertThat(sorted.stream().skip(lastBar + 1)).isEmpty();
    }
    
    @Property
    void noItemBetweenBars(@ForAll List<@From("withFooBars") String> list) {
        List<String> sorted = MySorter.sortFooBar(list);
        int firstBar = findFirst(sorted, item -> item.startsWith("bar"));
        int lastBar = findLast(sorted, item -> item.startsWith("bar"));
        if (firstBar < 0 && lastBar < 0) return;
        List<String> allBars = sorted.subList(
            Math.max(firstBar, 0),
            lastBar >= 0 ? lastBar + 1 : sorted.size()
        );
        Assertions.assertThat(allBars).allMatch(item -> item.startsWith("bar"));
    }
    
    @Provide
    Arbitrary<String> withFooBars() {
        Arbitrary<String> postFix = Arbitraries.strings().alpha().ofMaxLength(10);
        return Arbitraries.oneOf(
            postFix, postFix.map(post -> "foo" + post), postFix.map(post -> "bar" + post)
        );
    }
    
    int findFirst(List<String> list, Predicate<String> condition) {
        for (int i = 0; i < list.size(); i++) {
            String item = list.get(i);
            if (condition.test(item)) {
                return i;
            }
        }
        return -1;
    }
    
    int findLast(List<String> list, Predicate<String> condition) {
        for (int i = list.size() - 1; i >= 0; i--) {
            String item = list.get(i);
            if (condition.test(item)) {
                return i;
            }
        }
        return -1;
    }
}

这是一个符合规范的简单实现:

class MySorter {
    static List<String> sortFooBar(List<String> in) {
        ArrayList<String> result = new ArrayList<>();
        int countFoos = 0;
        for (String item : in) {
            if (item.startsWith("foo")) {
                result.add(0, item);
                countFoos++;
            } else if (item.startsWith("bar")) {
                result.add(result.size(), item);
            } else {
                result.add(countFoos, item);
            }
        }
        return result;
    }
}

在这个示例中,属性的代码超过了实现的代码量。这可能是好事,也可能是坏事,取决于所需行为的复杂性。

英文:

I assume that you have some sorter function with signature

List&lt;String&gt; sortFooBar(List&lt;String&gt; list)

I see at least five properties that sortFooBar(list) should fulfill:

  1. Keep all items - and only those - in the list
  2. No item before first "foo"
  3. No other items between first and last "foo"
  4. No item after last "bar"
  5. No other item between first and last "bar"

In a real functional language those properties are all rather easy to formulate in Java it requires a bit of code. So here's my take on the problem using jqwik as PBT framework and AssertJ for assertions:

import java.util.*;
import java.util.function.*;
import org.assertj.core.api.*;
import net.jqwik.api.*;

class MySorterProperties {
    
    @Property
    void allItemsAreKept(@ForAll List&lt;@From(&quot;withFooBars&quot;) String&gt; list) {
    	List&lt;String&gt; sorted = MySorter.sortFooBar(list);
    	Assertions.assertThat(sorted).containsExactlyInAnyOrderElementsOf(list);
    }
    
    @Property
    void noItemBeforeFoo(@ForAll List&lt;@From(&quot;withFooBars&quot;) String&gt; list) {
    	List&lt;String&gt; sorted = MySorter.sortFooBar(list);
    	int firstFoo = findFirst(sorted, item -&gt; item.startsWith(&quot;foo&quot;));
    	if (firstFoo &lt; 0) return;
    	Assertions.assertThat(sorted.stream().limit(firstFoo)).isEmpty();
    }
    
    @Property
    void noItemBetweenFoos(@ForAll List&lt;@From(&quot;withFooBars&quot;) String&gt; list) {
    	List&lt;String&gt; sorted = MySorter.sortFooBar(list);
    	int firstFoo = findFirst(sorted, item -&gt; item.startsWith(&quot;foo&quot;));
    	int lastFoo = findLast(sorted, item -&gt; item.startsWith(&quot;foo&quot;));
    	if (firstFoo &lt; 0 &amp;&amp; lastFoo &lt; 0) return;
    	List&lt;String&gt; allFoos = sorted.subList(
    		Math.max(firstFoo, 0),
    		lastFoo &gt;= 0 ? lastFoo + 1 : sorted.size()
    	);
    	Assertions.assertThat(allFoos).allMatch(item -&gt; item.startsWith(&quot;foo&quot;));
    }
    
    @Property
    void noItemAfterBar(@ForAll List&lt;@From(&quot;withFooBars&quot;) String&gt; list) {
    	List&lt;String&gt; sorted = MySorter.sortFooBar(list);
    	int lastBar = findLast(sorted, item -&gt; item.startsWith(&quot;bar&quot;));
    	if (lastBar &lt; 0) return;
    	Assertions.assertThat(sorted.stream().skip(lastBar + 1)).isEmpty();
    }
    
    @Property
    void noItemBetweenBars(@ForAll List&lt;@From(&quot;withFooBars&quot;) String&gt; list) {
    	List&lt;String&gt; sorted = MySorter.sortFooBar(list);
    	int firstBar = findFirst(sorted, item -&gt; item.startsWith(&quot;bar&quot;));
    	int lastBar = findLast(sorted, item -&gt; item.startsWith(&quot;bar&quot;));
    	if (firstBar &lt; 0 &amp;&amp; lastBar &lt; 0) return;
    	List&lt;String&gt; allFoos = sorted.subList(
    		Math.max(firstBar, 0),
    		lastBar &gt;= 0 ? lastBar + 1 : sorted.size()
    	);
    	Assertions.assertThat(allFoos).allMatch(item -&gt; item.startsWith(&quot;bar&quot;));
    }
    
    @Provide
    Arbitrary&lt;String&gt; withFooBars() {
    	Arbitrary&lt;String&gt; postFix = Arbitraries.strings().alpha().ofMaxLength(10);
    	return Arbitraries.oneOf(
    		postFix, postFix.map(post -&gt; &quot;foo&quot; + post), postFix.map(post -&gt; &quot;bar&quot; + post)
    	);
    }
    
    int findFirst(List&lt;String&gt; list, Predicate&lt;String&gt; condition) {
    	for (int i = 0; i &lt; list.size(); i++) {
    		String item = list.get(i);
    		if (condition.test(item)) {
    			return i;
    		}
    	}
    	return -1;
    }
    
    int findLast(List&lt;String&gt; list, Predicate&lt;String&gt; condition) {
    	for (int i = list.size() - 1; i &gt;= 0; i--) {
    		String item = list.get(i);
    		if (condition.test(item)) {
    			return i;
    		}
    	}
    	return -1;
    }
}

And this is a naive implementation that is consistent with the spec:

class MySorter {
static List&lt;String&gt; sortFooBar(List&lt;String&gt; in) {
ArrayList&lt;String&gt; result = new ArrayList&lt;&gt;();
int countFoos = 0;
for (String item : in) {
if (item.startsWith(&quot;foo&quot;)) {
result.add(0, item);
countFoos++;
} else if (item.startsWith(&quot;bar&quot;)) {
result.add(result.size(), item);
} else {
result.add(countFoos, item);
}
}
return result;
}
}

In this example the code for the properties exceeds the amount of code for the implementation. This might be good or bad depending on how tricky the desired behaviour is.

huangapple
  • 本文由 发表于 2020年6月29日 13:30:44
  • 转载请务必保留本文链接:https://go.coder-hub.com/62631791.html
匿名

发表评论

匿名网友

:?: :razz: :sad: :evil: :!: :smile: :oops: :grin: :eek: :shock: :???: :cool: :lol: :mad: :twisted: :roll: :wink: :idea: :arrow: :neutral: :cry: :mrgreen:

确定