【问题标题】:Developing to an interface with TDD使用 TDD 开发接口
【发布时间】:2009-02-13 02:07:27
【问题描述】:

我是 TDD 的忠实拥护者,这些天来我的大部分开发工作都使用它。但是,我经常遇到的一种情况,并且从未找到我认为是“好”的答案,类似于以下(人为的)示例。

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

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

    int getShortestPathLength(GraphNode start, GraphNode goal);
}

现在,假设我要创建此接口的三个实现。我们称它们为DijkstraPathFinderDepthFirstPathFinderAStarPathFinder

问题是,如何使用 TDD 开发这三个实现?它们的公共接口将是相同的,并且大概我会为每个接口编写相同的测试,因为 getShortestPath() 和 getShortestPathLength() 的结果在所有三个实现中应该是一致的。

我的选择似乎是:

  1. 在我编写第一个实现的代码时,针对PathFinder 编写一组测试。然后“盲目”编写其他两个实现并确保它们通过PathFinder 测试。这似乎不对,因为我没有使用 TDD 来开发后两个实现类。

  2. 以测试优先的方式开发每个实现类。这似乎不对,因为我将为每个类编写相同的测试。

  3. 结合以上两种技术;现在我有一组针对接口的测试和一组针对每个实现类的测试,这很好,但是测试都是一样的,这不好。

这似乎是一种相当普遍的情况,尤其是在实现策略模式时,当然实现之间的差异可能不仅仅是时间复杂度。其他人如何处理这种情况?是否有针对我不知道的接口进行测试优先开发的模式?

【问题讨论】:

    标签: language-agnostic tdd polymorphism


    【解决方案1】:

    您编写接口测试来运行接口,并为实际实现编写更详细的测试。 Interface-based design 谈到了这样一个事实,即您的单元测试应该为该接口形成一种“合同”规范。也许当 Spec# 出来时,会有一种语言支持的方式来做到这一点。

    在这种特殊情况下,这是一个严格的策略实现,接口测试就足够了。在其他情况下,接口是实现功能的子集,您将对接口和实现进行测试。例如,考虑一个实现 3 个接口的类。

    编辑:这很有用,因此当您在以后添加接口的另一个实现时,您已经有测试来验证该类是否正确实现了接口的契约。这适用于像 ISortingStrategy 这样特定的东西,也适用于像 IDisposable 这样广泛的东西。

    【讨论】:

    • 我还是有问题。我采用了测试驱动两个相同类的方法,然后重构为一个通用接口。添加第三个类时,我剪切并粘贴了测试,并通过将接口添加到类中来依次编译并变为绿色。如果我的剪切粘贴出错,这很容易出错,但这不是我的问题。现在我的情况是,我可以使用测试从一个类向接口添加功能,但该功能在其他类中将没有测试来支持它。我必须记住复制测试。这似乎不对?
    • 将我的评论拉到一个新问题中:stackoverflow.com/questions/1340712/…
    【解决方案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
    

    我认为这是最省力的解决方案,非常符合敏捷/TDD 原则。

    【讨论】:

      【解决方案3】:

      我对选项 1 没有问题,请记住,重构是 TDD 的一部分,通常在重构阶段您会转向策略等设计模式,所以我不会为此感到难过无需编写新测试。

      如果您想测试每个 PathFinder impl 的实现特定细节,您可以考虑传递模拟 GraphNode,这些 GraphNode 能够以某种方式帮助断言实现的 Dijkstra-ness 或 DepthFirst-ness 等。 (也许这些模拟 GraphNode 可以记录它们是如何被遍历的,或者以某种方式测量性能。)也许这是测试过度,但是如果你知道你的系统出于某种原因需要这三种不同的策略,那么进行测试可能会很好说明原因 - 否则为什么不只选择一个实现并丢弃其他实现?

      【讨论】:

        【解决方案4】:

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

        【讨论】:

          【解决方案5】:

          这似乎不对,因为我是 不使用 TDD 开发第二个 两个实现类。

          你确定。

          首先注释掉除一个之外的所有测试。当您使测试通过时,重构或取消注释另一个测试。

          jtf

          【讨论】:

            猜你喜欢
            • 2011-03-15
            • 2013-03-31
            • 1970-01-01
            • 1970-01-01
            • 1970-01-01
            • 1970-01-01
            • 2010-09-10
            • 2011-02-11
            • 1970-01-01
            相关资源
            最近更新 更多