使用TDD:自上而下和自下而上

23

由于我是一个TDD新手,我目前正在开发一个微小的C#控制台应用程序来进行实践(因为熟能生巧,对吧?)。我开始制作了一个简单的草图,展示了应用程序可能如何组织(按类别),然后逐个开发所有可以识别的域类(当然是先测试)。

最终,这些类必须被整合在一起,才能使应用程序可运行,即将必要的代码放置在Main方法中,调用必要的逻辑。但是,我不知道如何以“测试优先”的方式完成这个最后的集成步骤。

我想,如果我采用了“自上而下”的方法,我就不会遇到这些问题了。问题是:我该怎么做呢?我应该从测试Main()方法开始吗?

如果有人能给我一些指导,我将非常感激。


你有任何高级测试用例吗?如果通过了,将确认应用程序正在工作并且问题已解决?有什么可以证明整个事情都在运行(当然要排除UI)。(我也是TDD新手。) - Abhijeet Kashnia
我没有任何高级测试用例,也许这就是缺失的东西。唯一的问题是:这样一个高级别的测试看起来会是什么样子?该应用程序只在控制台中生成文本。我应该断言正在写入的内容吗? - Chris
当你说“集成”时,这并不意味着你想要测试对象的连接是否正确,而是所有类一起按预期工作。现在,大多数人建议您应该有一些测试用例,告诉您是否完成了工作。正如Marcus指出的那样,主函数中的内容可能是一个测试,一个积极的测试。你看,主方法是测试驱动应用程序中编写的最后一个方法。我建议你看一下验收测试。这本书对你可能会很有趣:http://www.growing-object-oriented-software.com/ - Abhijeet Kashnia
是的,为了澄清,我写了“integration”,但是实际上我的意思是“连接”。我现在正在阅读那本书,他们似乎推荐您应该设置像您描述的接受测试。我想这意味着我应该测试实际被打印到控制台上的内容,例如错误消息? - Chris
7个回答

34

“自上而下”是已经被用于计算机领域来描述分析技术。我建议使用“自外而内”的术语。

“自外而内”是BDD中的一个术语,其中我们认识到系统通常有多个用户界面,用户可以是其他系统以及人。BDD方法类似于TDD方法,我会简要介绍一下,希望对您有所帮助。

在BDD中,我们从一个场景开始 - 通常是一个用户使用系统的简单示例。围绕这些场景的讨论可以帮助我们确定系统应该做什么。我们编写一个用户界面,如果需要,我们可以自动化地执行这些场景来测试该用户界面。

当我们编写用户界面时,尽可能保持简洁。用户界面将使用另一个类 - 控制器、视图模型等 - 我们可以为其定义一个API。

在这个阶段,API可能是一个空类或一个(程序)接口。现在我们可以编写用户界面如何使用控制器的示例,并展示控制器如何提供价值。

这些示例还展示了控制器的职责范围,以及它如何将其职责委派给其他类,比如存储库、服务等。我们可以使用模拟来表达这种委派。然后,我们编写该类以使示例(单元测试)工作。我们只编写足够的示例以使系统级场景通过。
我发现重构模拟示例很常见,因为模拟的接口一开始只是猜测的,然后随着类的编写而更加完整。这将帮助我们定义下一层接口或API,为其描述更多示例,直到不再需要模拟,并且第一个场景通过。
随着我们描述更多场景,我们在类中创建不同的行为,我们可以进行重构以消除不同场景和用户界面需要相似行为的重复。
通过以外部为基础的方式,我们可以尽可能地获取有关API应该是什么的信息,并尽快重新设计这些API。这符合实际选择(除非你知道为什么,否则不要过早承诺)的原则。我们不创建我们不使用的任何东西,而API本身是为可用性而设计的,而不是为了编写的便利性。代码往往采用更自然的风格,使用领域语言而不是编程语言编写,使其更易读。由于代码的阅读量大约是编写量的10倍,因此这也有助于使其易于维护。
因此,我会使用外部为基础的方法,而不是自下而上的智能猜测。我的经验是,它会产生更简单、更强烈解耦、更易读和更易于维护的代码。

这非常有帮助,谢谢!听起来我应该使用覆盖性验收测试来驱动我的应用程序。 - Chris
1
我发现单词“test”往往会让人们想到固定事物、防止事物被破坏、确保事物正常工作等。相反,试着描述一些关于事物如何工作的例子。你正在帮助其他人理解你的代码价值以及行为如何提供价值,以便他们可以安全地进行更改。这就是为什么我(和其他BDD者)使用“场景”和“示例”这些词。希望这样说得清楚。只有在编写代码后才会变成测试。 - Lunivore

2
如果你将main()函数之外的东西移出去,你是否不需要测试该函数?
这是有道理的,因为你可能想要使用不同的cmd-args运行,并且你希望测试它们。

是的,我可以这样做,这可能是目前最简单的事情。然而,在开始时,我不应该从一些更高级别的测试开始,以便驱动应用程序吗?感觉是这样,但也许我想太多了。 - Chris
规定控制台中的输入/输出到文件是一种方法,但我认为这更适用于回归测试,不过因人而异。Mercurial DVCS项目在其套件中确实执行了此操作,并且对他们来说效果非常好,但他们有相当好的命令来检查任何操作的结果。 - Macke

0
您也可以测试您的控制台应用程序。我觉得这很困难,看看这个例子:
[TestMethod]
public void ValidateConsoleOutput()
{
    using (StringWriter sw = new StringWriter())
    {
        Console.SetOut(sw);
        ConsoleUser cu = new ConsoleUser();
        cu.DoWork();
        string expected = string.Format("Ploeh{0}", Environment.NewLine);
        Assert.AreEqual<string>(expected, sw.ToString());
    }
}

你可以在这里查看完整的帖子


0
主函数最终应该非常简单。我不知道在C#中它是什么样子,但在C++中应该看起来像这样:
#include "something"

int main( int argc, char *argv[])
{
  return TaskClass::Run( argc, argv );
}

将已创建的对象传递给类的构造函数,但在单元测试中传递模拟对象。

有关TDD的更多信息,请查看这些录屏。它们解释了如何进行敏捷开发,还介绍了如何使用c#进行TDD,并提供了示例。


0
“自下而上”的方法可以节省很多工作,因为您已经有了低级别的类(经过测试),并且可以在更高级别的测试中使用它们。相反,如果您采用“自上而下”的方法,则需要编写大量(可能是复杂的)模拟。此外,“自上而下”的方法通常不起作用,因为您会想出一些不错的高级模型,进行测试和实现,然后向下移动时,您会意识到某些假设是错误的(即使对于经验丰富的程序员来说,这也是相当典型的情况)。当然,设计模式可以帮助解决这个问题,但它也不是万能的解决方案。
为了实现完整的测试覆盖,你需要在任何情况下都对主函数进行测试。但我不确定在所有情况下这是否值得付出努力。只需将所有逻辑从主函数移至单独的函数(如Marcus Lindblom所建议的),对其进行测试,并让主函数仅将命令行参数传递给该函数即可。

控制台输出是一种最简单的测试能力,如果您使用TDD,它可以仅用于输出最终结果而无需进行任何诊断和调试消息。因此,请确保您的顶层函数返回可靠的结果,在Main中对其进行测试并输出。


0

我推荐“自上而下”(我称之为高层测试)。我写过相关内容:

http://www.hardcoded.net/articles/high-level-testing.htm

是的,你应该直接测试控制台输出。当然,最初设置测试以便直接测试控制台输出可能有些麻烦,但如果创建适当的帮助程序代码,下一个“高级”测试将更容易编写。通过这样做,你可以拥有无限的重构潜力。采用“自下而上”的方法,你的初始类关系非常严格,因为改变关系意味着改变测试(需要大量工作,也很危险)。


0

MSDN杂志12月号中有一篇有趣的文章,描述了“BDD周期如何将传统的测试驱动开发(TDD)周期与驱动单元级实现的特性级测试结合起来”。细节可能过于技术化,但思想和流程概述听起来与您的问题相关。


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