考虑一下这种方法,我想为它写一个单元测试。
public void update(List toUpdate) {
//...
Collections.sort(toUpdate, new SpecificComparator());
//...
}
我想确保我们使用 SpecificComparator 实现对给定列表进行排序。无论如何要这样做?
我正在考虑使用工厂来检索 SpecificComparator 实现,以便我可以与 Mockito 验证我们正在调用工厂,但这并不能确保我根据这个比较器对列表进行排序,我们可以想象有人在方法中调用工厂但不对列表进行排序......
或者,我也可以验证toUpdate
list 对象的顺序,在我的单元测试中订购具有相同比较器的另一个列表并检查它们是否以相同的方式排序?
这么多的答案,都包含一条"真相",但缺少其他内容。伤心。
因此:您应该绝对避免使用模拟来测试您的比较器对调用进行排序。相反,您需要测试两个方面:
- 为比较器编写单元测试。哎呀,比较器有一种方法;具有非常明确的语义。您专注于编写单元测试来隔离测试该内容。因为这是A)容易做到的,B)单元测试的整个思想:你对最小的单元进行尽可能多的测试。
- 你编写一些单元测试来确保你的"管道"是正确的。而且你不会嘲笑那里的任何东西。您确保使用比较器对具有已知内容的列表进行排序;然后你
assertThat(actualList, is(expectedList));
长话短说:比较器本身是一个单元。你开始测试那个东西;与所有极端情况什么都没有。然后,确保应该对列表进行排序的方法确实返回排序列表。
如果你的代码是这样的,你仍然需要考虑模拟,那么你的设计很可能可以改进为易于测试(而不是难以测试)。
我想说这不是一件合理的事情。
单元测试应该只在其公共接口上测试一个类,并且由于比较器是一个隐藏的实现细节,因此如何实现排序顺序与单元测试无关。
这意味着,如果有一天实现发生变化,通过其他方式实现相同的排序顺序,测试将继续通过 - 这就是事情应该的样子。
因此,您应该测试输出是否按预期顺序排序,但不应断言有关该方法私有对象的任何内容。
当然,如果您将Comparator
作为类 API 的一部分,那就另当别论了:
public class Updater {
private final Comparator sortComparator;
public Updater(Comparator sortComparator) {
this.sortComparator = sortComparator;
}
public void update(List toUpdate) {
//...
Collections.sort(toUpdate, sortComparator);
//...
}
}
。然后,将模拟Comparator
传递给构造函数并断言它已被使用是合适的 - 因为现在调用者感兴趣,它传入Comparator
是正在使用的,因此应该进行测试。
这个想法是让列表已经排序,就像使用了特定的比较器一样。这很重要,因为比较器的行为不应改变,如果改变,则应编写新的要求,这意味着用于测试的列表必须改变。但是,在大多数情况下,您不应该过于频繁地更改单个比较器的行为。因此,您应该创建已经排序的列表,就像使用了比较器一样,然后您可以调用以下方法
public boolean unitTestComparator(List sorted, Comparator comp)
{
List shuffled = new List(sorted);
Collections.shuffle(shuffled);
return sorted.equals(Collections.sort(shuffled, comp));
}
现在,您可以对各种列表使用多个测试,以执行不同比较器的所有边缘情况。所有这一切的关键是你知道使用比较器后列表应该是什么样子,唯一乏味的部分是找到每个比较器的所有边缘情况。您还可以运行 for 循环,并在此方法上根据需要多次对其进行测试,因为您为该方法提供了正确的列表格式。它所做的只是随机化洗牌。
请注意,这是随机测试,您还可以向方法添加另一个参数,该参数可能是以您想要的方式洗牌的列表,以查找特定的边缘情况。
你上面提到的我称之为"交叉测试",但我认为没有必要,如果你真的想实现它,我把它给你,让你多想想。 使用不同的比较器测试更新列表两次,并断言结果是预期的。
public class CrossingTest {
@Test
public void sortWithSpecificComparator() throws Throwable {
List<Integer> list = asList(0, 1, 2);
List<Integer> reversedList = asList(2, 1, 0);
Comparator<Integer> originalOrder = (a, b) -> a < b ? -1 : 1;
assertThat(update(list, with(originalOrder)), equalTo(list));
assertThat(update(list, with(originalOrder.reversed()))
, equalTo(reversedList));
}
private List<Integer> update(List<Integer> input, Comparator comparator) {
List<Integer> list = new ArrayList<>(input);
SUT it = new SUT(comparator);
it.update(list);
return list;
}
private Comparator with(Comparator comparator) { return comparator; }
}
class SUT {
private Comparator comparator;
public SUT(Comparator comparator) {
this.comparator = comparator;
}
public void update(List toUpdate) {
Collections.sort(toUpdate, comparator);
}
}
尽管您可以使用以下内容模拟新对象:
SpecificComparator comparator = Mockito.mock(SpecificComparator.class);
Mockito.whenNew(SpecificComparator.class).thenReturn(comparator);
最佳方法始终是在调用方法后检查元素的顺序,而不是检查何时/如何使用比较器。您可以使用Listequals
方法做到这一点,这就是它所说的:
将指定的对象与此列表进行比较以确保相等。返回 当且仅当指定的对象也是列表时为 true,两个列表 具有相同的大小,并且两者中所有对应的元素对 列表是平等的。