使用测试驱动开发(TDD)开发接口。

4

我是TDD的忠实粉丝,现在在大部分开发工作中都使用它。但是有一种情况我经常遇到,却从未找到一个我认为“好”的答案,就像下面这个(编造的)例子。

假设我有一个接口,像这样(用Java写的,但实际上,适用于任何OO语言):

public interface PathFinder {
    GraphNode[] getShortestPath(GraphNode start, GraphNode goal);

    int getShortestPathLength(GraphNode start, GraphNode goal);
}

现在,假设我想创建这个接口的三个实现。我们称它们为DijkstraPathFinder、DepthFirstPathFinder和AStarPathFinder。
问题是,我如何使用TDD开发这三种实现?它们的公共接口将是相同的,并且我会为每个编写相同的测试,因为getShortestPath()和getShortestPathLength()的结果应在所有三个实现中保持一致。
我的选择似乎是:
1. 在编写第一个实现时,针对PathFinder编写一组测试。然后“盲目”编写另外两个实现,并确保它们通过PathFinder测试。这似乎不正确,因为我没有使用TDD来开发第二和第三个实现类。 2. 以测试优先的方式开发每个实现类。这似乎不正确,因为我将为每个类编写相同的测试。 3. 将上述两种技术结合起来;现在我有一组针对接口的测试和一组针对每个实现类的测试,这很好,但测试都是相同的,这不好看。
这似乎是一个相当常见的情况,特别是在实现策略模式时,当然,实现之间的差异可能不仅仅是时间复杂度。其他人如何处理这种情况?是否有一种针对接口的测试优先开发模式,我不知道?
5个回答

3
您编写接口测试来测试接口,您还会为实际实现编写更详细的测试。基于接口的设计谈到了您的单元测试应该形成该接口的“合同”规范的事实。也许在Spec#发布后,会有一种语言支持的方式来做到这一点。
在这种特定情况下,即严格的策略实现中,接口测试就足够了。在其他情况下,如果一个接口是实现功能的子集,那么您将同时为接口和实现编写测试。例如,考虑一个实现3个接口的类。
编辑:这样做很有用,因此当您在未来添加另一个接口实现时,您已经有了验证该类正确实现接口合同的测试。这适用于像ISortingStrategy这样具体的东西,也适用于像IDisposable这样广泛的东西。

我仍然有问题。我的方法是测试驱动两个相同的类,然后重构为一个公共接口。当添加第三个类时,我复制并粘贴了测试,并通过将接口添加到类中逐个编译和测试使其变绿。如果我的复制和粘贴出错,这种方法就容易出现错误,但这不是我的问题。现在我面临的情况是,我可以使用测试来向一个类的接口添加功能,但其他类将没有支持该功能的测试。我必须记得复制测试。这似乎不太对? - tenpn
将我的评论分成一个新的问题:http://stackoverflow.com/questions/1340712/how-to-tdd-functionality-in-a-base-mixin-class-from-one-of-many-leaf-classes - tenpn

2

我认为选择方案1是没有问题的。需要记住的是,重构是TDD的一部分,通常在重构阶段才会转移到设计模式,比如策略模式。所以,如果没有编写新的测试,也不必感到难过。

如果您想要测试每个PathFinder实现的特定实现细节,可以考虑传递模拟的GraphNodes,这些节点可以帮助断言实现的Dijkstra-ness或DepthFirst-ness等。也许这是过度测试,但是如果您知道您的系统出于某种原因需要这三种不同的策略,那么最好有测试来证明为什么 - 否则为什么不只选择一个实现并丢弃其他实现呢?


2

针对接口编写测试,并在每个实现中重用它们是没有问题的,例如:

public class TestPathFinder : TestClass
{
    public IPathFinder _pathFinder;
    public IGraphNode _startNode;
    public IGraphNode _goalNode;

    public TestPathFinder() : this(null,null,null) { }
    public TestPathFinder(IPathFinder ipf, 
        IGraphNode start, IGraphNode goal) : base()
    {
        _pathFinder = ipf;
        _startNode = start;
        _goalNode = goal;
    }
}

TestPathFinder tpfDijkstra = new TestPathFinder(
    new DijkstraPathFinder(), n1, nN);
tpfDijkstra.RunTests();

//etc. - factory optional

我认为这是“最小工作量”解决方案,非常符合敏捷/测试驱动开发原则。

1

我不介意将测试代码作为新测试的模板,只要它们具有类似的功能。根据被测试的特定类,您可能需要使用不同的模拟对象和期望来重新设计它们。至少,您将不得不重构它们以使用新的实现。然而,我会遵循TDD方法,先取一个测试,为新类重新设计它,然后编写仅能通过该测试的代码。这可能需要更多的纪律,因为您已经掌握了一种实现方式,并且无疑会受到您已经编写的代码的影响。


1
这似乎不太对,因为我没有使用TDD来开发后两个实现类。
当然你是的。
首先,将除一个之外的所有测试注释掉。在使一个测试通过时,要么重构代码,要么取消注释另一个测试。
Jtf

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接