TDD,如何编写即使对象不存在也能编译的测试

8

我正在使用 VS 2012,但这并不是很重要。

重要的是我正在尝试通过先编写所有测试,然后再创建代码来进行一些 TDD。

然而,应用程序将无法编译,因为我的对象或方法不存在。

现在,在我看来,我应该能够创建所有的测试,但仍然可以运行我的应用程序,以便我可以进行调试等操作。测试不应该因为缺少对象和方法而阻止编译。

我认为它的整个意义在于,当你开发测试时,你可以开始看到重复等问题,以便在编写单行代码之前进行重构。

所以问题是,有没有办法做到这一点,或者我做错了?

编辑 我正在使用VS2012和C#。

7个回答

6

我看到一个小问题,就是先编写所有的测试,然后再创建代码。

你不需要先编写所有的测试用例,只需要一个,让它失败,通过并重复。这意味着在任何时候,你应该有一个失败的测试用例。

在这个意义上,编译错误也算作一个失败的测试用例。所以下一步是让它通过——也就是添加存根或返回默认值使其编译。此时该测试用例将会显示为红色...然后努力使其变为绿色。


5
测试驱动开发是关于非常小的迭代。你不需要预先定义所有的测试。你可以基于一个需求的一小部分来创建一个测试。然后实现代码以通过该测试。一旦它通过,你就可以继续处理另一部分需求。
这意味着试图在前期完成所有设计(无论是创建详细的类图还是创建一堆测试)意味着你会发现更改设计中的弱点太昂贵,因此你不会改进你的代码。
以下是一个例子。假设你决定使用继承来关联两个对象,但当你开始实现这些对象时,你发现这使得测试它们变得困难。你发现单独测试每个对象并通过包含关系将它们联系起来会更容易。正在发生的是测试正在驱动你的设计朝着更松散耦合的方向发展。这是TDD的一个非常好的结果 - 你正在使用测试来改进设计。
如果你事先编写了所有测试,假设你的继承设计决策是一个好选择,你要么会抛弃很多工作,要么会说“现在改变这样的决策太困难了,所以我只能接受这个次优的设计。”
你当然可以提前创建与业务规则相关的验收测试。那些被称为行为测试(BDD的一部分),它们对于从用户的角度测试软件的功能非常有用。但那些不是单元测试。单元测试是为了从开发者的角度测试代码。提前创建单元测试会破坏TDD的目的,因为它会使测试变得更加困难,阻止你改进你的代码,并经常导致反叛和失败。这就是为什么做好它很重要。

4
重要的是,我正在尝试通过先编写所有测试,然后再创建代码来进行TDD。问题在于,“先编写所有测试”绝对不是“进行TDD”。测试驱动开发包括许多“红-绿-重构”周期的小重复:
- 将单元测试添加到测试套件中,运行它并观察它失败(红色) - 仅向被测试系统添加足够的代码以使所有测试都通过(绿色) - 在保持所有测试通过的同时改进被测试系统的设计(通常是通过消除重复)(重构)
如果您一开始就编写整个庞大的测试套件,那么您将花费很长时间才能达到“绿色”(所有测试都通过)状态。
“但是,应用程序无法编译,因为我的对象或方法不存在。”
这是任何编译语言的典型问题;这不是TDD问题本身。这意味着,为了观察新测试失败,您可能必须编写一个最小的存根,以使编译器满意。
例如,我可以编写以下测试(使用NUnit):
[Test]
public void DefaultGreetingIsHelloWorld()
{
    WorldGreeter target = new WorldGreeter();
    string expected = "Hello, world!";
    string actual = target.Greet();
    Assert.AreEqual(expected, actual);
}

我需要编写大量的代码才能让应用程序编译,并使测试失败:

public class WorldGreeter
{
    public string Greet()
    {
        return String.Empty;
    }
}

一旦我构建出解决方案并看到了失败的测试,我可以添加代码使第一个测试通过:
    public string Greet()
    {
        return "Hello, world!";
    }

一旦所有测试都通过,我就可以查看受测系统并确定如何改进设计。但是,在进行重构之前,完成“红色”和“绿色”步骤对于TDD学科非常重要。
“我认为整个重点在于,随着您开发测试,您可以开始看到重复等问题,以便在编写单行代码之前进行重构。”(引用)
Martin Fowler将refactoring定义为“一种有纪律的技术,用于重新组织现有的代码体,更改其内部结构而不改变其外部行为”(强调添加)。如果您还没有编写任何代码,则没有什么可以重构的。
“所以问题是,是否有方法可以做到这一点,还是我做错了?”(引用)
如果您想进行TDD,则恐怕您做错了。您可能能够通过您正在做的事情交付出色的代码,但这不是TDD。无论是否存在问题,这都取决于您自己决定。

2

您应该能够创建具有存根函数的空类,对吗?

class Whatever {
    char *foo( const char *name ) {}

    int can_wibble( Bongo *myBongo ) {}
}

然后你就可以编译了。

啊,抱歉,我使用的是C#。我的错误。 - griegs
就像你说的那样,语言并不重要。只需编写函数定义,并使其不执行任何操作。然后就可以对其进行编译。 - Andy Lester
1
是的,所以在测试设计期间,确保有桩代码,并根据需要进行重构。 +1 - griegs

2
不,它关于编写足够的代码来验证所需用例的实现。您可以提前定义测试用例,但为了编写测试用例,您需要迭代地编写一个测试,并使其失败。然后编写一些代码以确保代码通过。然后重复此过程,直到覆盖所有测试用例。建立代码时,这是识别您的编程设计和错误的位置。极限编程适合您能够在不考虑情况下更改代码,因为测试基础保护您的要求。但是,在构建代码和测试库时通过发现设计问题和缺陷进行重构/重新设计测试用例是常见的。然而,我的个人意见是,在非常一般的情况下,无法编译的测试实际上是元测试失败,需要在继续之前进行校正。它告诉您编写一些代码!

是的,我理解你的想法,但那样做会导致我的代码质量不佳。先编写所有的测试用例可以识别问题区域、重复和糟糕的设计,我认为这是更好的选择。 - griegs

0

使用模拟对象,来自维基百科:模拟对象是模仿真实对象行为的受控对象。程序员通常创建模拟对象来测试其他对象的行为,就像汽车设计师使用碰撞测试假人来模拟人在车辆碰撞中的动态行为一样。

请参考this


啊对,没错,那也许可以行得通。可惜我需要模拟所有的东西。 - griegs
如果 Mock 被正确使用,它可以帮助你获得良好的设计。 - Matt

0

2
链接式的回答并不被鼓励,Stack Overflow上的答案应该是解决问题搜索的最终目的地(而不是另一个会随时间变得过时的参考站点)。请考虑在此处添加一个独立的摘要,将链接作为参考。 - Alex

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