【问题标题】:Unit testing of a shuffle method on a list对列表中的 shuffle 方法进行单元测试
【发布时间】:2014-05-31 07:42:24
【问题描述】:

考虑以下类:

public class Deck {
    private final Queue<Card> queue = new LinkedList<>();

    public Deck() { }

    public Deck(final Collection<Card> cards) {
        Objects.requireNonNull(cards);
        queue.addAll(cards);
    }

    public void add(final Card card) {
        Objects.requireNonNull(card);
        queue.add(card);
    }

    public void addAll(final Collection<Card> cards) {
        Objects.requireNonNull(cards);
        queue.addAll(cards);
    }

    public void shuffle() {
        Collections.shuffle((List<Card>)queue);
    }

    public Card take() {
        return queue.remove();
    }
}

如何对shuffle() 方法进行单元测试?我正在使用 JUnit 4 进行测试。

我有以下选择:

  1. 测试shuffle() 看看它不会产生异常。
  2. 测试 shuffle() 并检查牌组是否真的被洗牌了。

选项2的示例伪代码:

while notShuffled
    create new Deck
    take cards and check if they are shuffled

这里唯一的罪魁祸首是,在执行为选项 2(也继承了选项 1)编写的测试时,如果改组没有按预期工作,那么代码执行将永远不会停止。

我将如何解决这个问题?是否可以限制 JUnit 测试的执行时间?

【问题讨论】:

  • " 那么代码执行将永远不会停止。"
  • 我认为您没有考虑的另一个问题是 shuffle 方法是否保持列表的顺序相同。我想这取决于您对 shuffle 方法的期望。至于永远不会完成的方法,这在理论上是任何被测方法都存在的问题。我不确定您是否需要明确担心这一点。
  • @fge 现在添加了伪代码,没想到还不清楚。
  • @StealthRabbi(在编辑中澄清)我正在考虑 my Deck.shuffle 方法保持列表相同,但我不考虑 Collections.shuffle 实现可能被打破。
  • @Vakh 你的回答完全有效,IMO。想取消删除以收到我的支持吗?

标签: java unit-testing junit


【解决方案1】:

目前,您的班级是tightly coupled,带有Collections.shuffle 函数。静态函数因使事情更难测试而臭名昭著。 (最重要的是,你测试Collections.shuffle 毫无意义;大概它可以正常工作。)

为了解决这个问题,您可以在您的班级中引入seam 来实现这种洗牌功能。这是通过将shuffle 函数提取到角色(由接口表示)中来完成的。例如:

public interface ICardShuffler {
    void shuffle(List<Card> cards);
}

然后,您的Deck 类可以配置为保留对该接口的某些实现的实例的引用,并在必要时调用它:

public class Deck {
    private final Queue<Card> queue = new LinkedList<>();

    private ICardShuffler cardShuffler;

    public Deck(ICardShuffler cardShuffler) {
        this.cardShuffler = cardShuffler;
    }
    ...
    public void shuffle() {
        cardShuffler.shuffle((List<Card>)queue);
    }
    ...

这允许您的单元测试使用test double(如模拟对象)来验证预期行为是否发生(即shuffle 在提供的ICardShuffler 上调用shuffle)。

最后,您可以将当前功能移动到此接口的实现中:

public class CollectionsCardShuffler implements ICardShuffler {
    public void shuffle(List<Card> cards) {
        Collections.shuffle(cards);
    }
}

注意:除了方便测试之外,此接缝还允许您实现新的洗牌方法,而无需修改 Deck 中的任何代码。

【讨论】:

  • 好答案 WRT 设计。但是,我不认为仅仅验证在 shuffle 方法中调用了模拟就足够了。如果传递给洗牌器的列表与take 中迭代的列表不同,则两种方法之间的正确联系将不成立。我相信更好的解决方案是在shuffle 中使用默认行为并测试shuffle'stake 的影响。这是基于shuffle 的唯一要求是更改take 返回的卡片顺序的想法。您无法通过单独测试 shuffle 来测试此联系人。
  • @JohnB "如果传递给 shuffler 的列表与 take 中迭代的列表不同..." shuffle 的单元测试应该验证正确的列表正在传递给ICardShuffler.shuffle 方法。 (事实上​​,这是测试需要验证的唯一项。)如果该单元测试失败,则两种方法之间的约定无效(因为根据定义,shuffle 将是认为损坏)。
  • 不同意。似乎您想测试shuffle 的实现而不是两种方法之间的契约:suffletake。例如,如果我想重构代码以维护原始卡片列表,如果我想开始新游戏,我可以将不同的列表传递给洗牌器。
  • 实际上,我明确地尝试 not 来测试Collections.shuffle 的实现。 :-) 无论如何,如果您将不同的列表传递给ICardShuffler.shuffle,它会破坏take 的预期行为,就像您将不同的列表传递给Collections.shuffle 一样。
  • 如果传递给shuffle 的列表与take 中使用的列表相同,则不会。不要求这与类字段中的列表实例相同。另外,鉴于字段私有,您将如何验证是否通过了正确的列表?您将针对哪个列表实例进行验证?或者,如果您验证列表的内容,则无法保证该列表与take 中使用的列表相同。同样,唯一的要求是takeshuffle 之间的联系。所有其他的实现都可以在不中断联系或测试的情况下进行重构。
【解决方案2】:

我不明白你的伪代码...为什么要使用 while 循环?只需在甲板上调用洗牌。如果抛出异常,则测试失败。如果牌组的顺序相同,则测试失败。您还需要更多吗?

【讨论】:

  • while循环用于通过调用take检索卡片
  • 洗牌是否意味着顺序必须改变?
  • 如果顺序没有改变,那么这是一个糟糕的洗牌,但是好吧......你赢了语义之战。
  • @JohnB 我认为你需要更多的伪代码,因为它仍然不清楚。您是在暗示您正在创建多个套牌。
【解决方案3】:

您也可以编写自己的类 CollectionsHelper 并使用它来代替 Collections。

public class CollectionsHelper {
    private CollectionsHelper(){}
    private static boolean isTest = false;
    private static int n = 0;
    public static void shuffle(List<?> l){
        if(!isTest) Collections.shuffle(l);
        else Collections.shuffle(l, new Random(n));
    }
    public static void setTest(){
        isTest = true;
        n = 0;
    }
    public static boolean isTest(){
        return isTest;
    }
    public static void setSeedForTest(int seed){
        n = seed;
    }
}

在每个测试开始时,您可以调用 CollectionsHelper.setTest() 以使用确定性随机播放。 您的课程如下所示:

public class Deck {
    private final Queue<Card> queue = new LinkedList<>();

    public Deck() { }

    public Deck(final Collection<Card> cards) {
        Objects.requireNonNull(cards);
        queue.addAll(cards);
    }

    public void add(final Card card) {
        Objects.requireNonNull(card);
        queue.add(card);
    }

    public void addAll(final Collection<Card> cards) {
        Objects.requireNonNull(cards);
        queue.addAll(cards);
    }

    public void shuffle() {
        CollectionsHelper.shuffle((List<Card>)queue);
    }

    public Card take() {
        return queue.remove();
    }
}

【讨论】:

    【解决方案4】:

    我会做如下的事情:

    • 首先,我会让Deck 类实现Iterator&lt;Card&gt;
    • 其次,在第一次调用shuffletake 之后,我将使Deck 类不可变。 IE。不允许添加卡片。

    然后我会做这样的事情......

     static List<List<Card>> previousDecks = new ArrayList<>();
     static List<Card> inputList = createListOfCards();
    
     static{
          privousDecks.add(inputList);
     }
    
     @Test
     public void testShuffle(){
        Deck deck = new Deck(inputList);
        deck.shuffle();
    
        List<Card> shuffled = new ArrayList<>();
        Iterators.addAll(shuffled, deck);
    
        assertThat(shuffled, 
           IsIterableContainingInAnyOrder.containsInAnyOrder(inputList));
    
        for (List<Card> previouslySeen : previousDecks){
            assertThat(shuffled,
                CoreMatchers.not(
                IsIterableContainingInOrder.contains(previouslySeen )));
        }
        previousDecks.add(shuffled);
     }
    

    然后我会找到一种多次运行testShuffle 的方法,以确保随机播放不会每次都产生相同的结果。这可以通过多种方式完成。 Here 就是一个例子。

    作为一个仅供参考,我在这里使用 Hamcrest 和 Guava。

    Iterators

    Hamcrest

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2021-07-29
      • 2012-06-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2015-08-20
      相关资源
      最近更新 更多