测试驱动开发入门
摘要:
许多开发者想学习使用测试驱动开发实践,Brendan在本篇文章中说明了怎样在写代码之前编写测试。
引言
作为软件开发者,几乎都可能会遇到过围绕测试驱动开发方面的宣传。许多个人和组织现使用的已是过时而不是新近流行的理念。有许多的测试框架、模拟框架、注入容器和许多其他工具供测试者使用。也有各种不同的方法去进行测试,因此我首先指出除了我在本文中描述的方法外还有其他可行的方法。
在本篇文章中,我将使用一些工具,下面是下载地址:
Visual Studio 2008:开发环境
RhinoMocks:模拟框架
NUnit:测试框架
首先创建测试
有种说法来自与人们经常听到的在他们写代码之前应先写测试时说的话,“这正好会创建一个编译错误”。这是对的,但记住我说过做事情有各种各样的方式。如果你不想这样的话里你不必提前创建测试,但我建议你这样做。我们为什么首先要创建测试至少出于两个原因:它有助于确保我们所做的工作的正确性,并且也提供出了测试的代码所期望的接口。
确保测试的准确性
我要告诉你的是,在有些地方,你要写你预期失败的测试方法而不是只让它通过。这有太多的原因会导致这样的情况,但这样的话在每天结束的时候,你写的测试并不能准确地验证你的代码。这样所有这些情况都会让测试错误的通过:互换了小于或者大于符号,忘记了断言语句,或者有时事情并不是按你所预期的进行。测试先行让你能确保你做的每件事情的正确性。
创建期望的接口
有时开发者要花费大量的时间去试图找出他们的代码所想要使用的接口。你想怎样在一批代码间进行交互和使用呢?最佳途径之一是找出它的一个用例并尝试去使用它。测试给出的是大的用例:他们说明了怎样去使用一个特定的类或者方法以及处理的结果是什么。因此我们如果假想测试使用了我们的类,在我们写类之前,我们就能给出想用来访问该类的接口。这意味着什么呢?这意味着我们现在知道了怎样去定义之间的交互接口,因为我们已经试图去使用过它。
假如我们没有这样做的话,我们会基于一个假设去定义我们应该怎样去和我们打算创建的代码进行交互。要记住在代码修改之前写测试的价值。我认为这是有益的,但是这只是其中的一个观点,你可以轻松地以其他的方式进行。
创建两个简单的测试
要开始了,我们想要去做的事情很容易,就是去验证我们想测试的东西的类型以及我们怎样去测试它们。在我们深入我们的第一个测试之前,我会尽可能给出我们的应用程序范例方面的一小点背景知识。我们打算写一个扑克牌游戏。从本文章的目的出发,我们将把我们的关注点放在游戏中的几个类上:Deck、Card和Player。
我们应该始终以最简单的方式去做我们想做的事情。我们需要做的第一件事是找出我们的代码应该完成的任务。然后我们想写一个测试去断言我们的代码是以所期望的方式执行。
为你的测试取名是极其重要的。假如你没有很好地对你的测试命名,在以后你将不知道它测试的是什么并且你可能会突然离题而去测试完全不同的东西。你要让描述能充分说明问题,但你也需要简洁。对于这第一个测试,我们将使用DeckCountShouldEqualCardCount这个名称。我们接下来的是一个称为DeckShouldHaveOneLessCardAfterDrawing的测试。有人读到这些名称时会认为我疯了,但我要提醒你的是,你要为每一个类都进行测试而它们都没有名称,当测试失败的时候你不知道是哪里发生了错误。这种具有描述性的名称会马上告诉你是哪里失败了,因为这些信息就包含在名称中。
我们知道,我们需能绘制扑克牌并且在我们绘制好一张牌后其一副牌就应该减少一张,因此我们应该先创建一副牌的集合并跟踪其集合的数目。然后,我们绘制了一副牌的集合中的一张牌后要断言这个集合中的扑克牌的数目应该减一。以下是我们的第一个测试方法。
清单1:简单的测试
public void DeckCountShouldEqualCardCount()
{
var cards = new List<Card> {new Card(), new Card(), new Card()};
int initialCardCount = cards.Count;
var deck = new Deck(cards);
Assert.AreEqual(initialCardCount, deck.Count);
}
public void DeckShouldHaveOneLessCardAfterDrawing()
{
var cards = new List<Card> {new Card(), new Card(), new Card()};
int initialCardCount = cards.Count;
var deck = new Deck(cards);
Card card = deck.DrawCard();
Assert.AreEqual(initialCardCount - 1, deck.Count);
}
当我们编译它的时候,我们会发现有生成错误因为我们还有点代码要去实现。我们能够猜到我们的这3个类现在还是个空壳,因此剩下的工作就是去为容纳扑克牌的Deck类创建一个构造函数、一个名称为DrawCard的方法以及一个名称为Count的属性,以便修复该编译错误,当我们首次创建它们的时候我们可以让它们每一个都抛出NotImplementedExceton。然后我们将就不再有编译错误。这时是一个很好地运行测试来确认它们的确是没有通过的机会。这意味着我们需要写些代码。失败的测试看起来有点像队列中的任务。
我不打算在本文章中涉及运行NUnit方面的知识,有大量的文章和教程对怎样运行这个应用程序进行过演示。
让测试通过
我可不想花费大量的时间来谈论怎样实现我们的代码来让测试通过,因为这不是本文章的关注点。现在我仅会包含一个快速的解决办法来让测试通过。请记住,在每一步,我们应该只写出正好能让测试通过的代码就足够了。我们将立即写出更多的测试来帮助我们创建更多的功能。如果我们已用代码充实了Deck类并能正常工作,那么我们最终完成的东西和下面的代码类似。
清单2:简单的实现
{
private readonly List<Card> _cards;
public Deck(List<Card> cards)
{
_cards = cards;
}
public int Count
{
get { return _cards.Count; }
}
public Card DrawCard()
{
Card drawnCard = _cards[0];
_cards.Remove(drawnCard);
return drawnCard;
}
}
注意,对于这些代码仍然还有大量的Bug产生。例如,如果Deck为空,我们就将得到一个ArgumentOutOfRangeException异常。我们需要完成更多的测试并让它们通过。
创建更多的测试
通常,我喜欢让一个测试对常见的情况以及例外的情况都进行测试,我喜欢创建能反映怎样去使用类的测试。我们至少要有一个预期的情况和非预期的情况。预期的情况指的是任何事情都按预期运行,显然非预期的情况正好相反。而这可能意味着我们有异常抛出或者我们要处理发生的非预期的情况。因为它们很容易创建,我将首先处理一种特殊情况.我们现在需要反问自己假如我们运行时超出了扑克牌集合的界限会发生什么。有了答案后,我们应该创建一个测试,它精确地说明了这些情况。
清单3:处理特定情况的测试
public void EmptyDeckShouldReturnNullWhenDrawing()
{
// Create an empty Deck
var deck = new Deck(new List<Card>());
Card card = deck.DrawCard();
Assert.AreEqual(null, card, "A Card was returned from an empty Deck.");
}
我们在这里做的一个重要的事情是,我们定义了我们期望在这里发生些什么。然而,这显然是不期望的,我们现在得到了一个异常。一旦我们测试并通过,我们就知道任何调用这个方法的地方的预期结果。当我们执行这个方法的时候,我们知道我们会得到该异常,这很棒。System.ArgumentOutOfRangeException让我们知道应用程序已按我们所知晓的方式工作,只是我们还没有写代码来让这个测试通过。此时我们将创建多方面的测试。我们将新增一个确信我们已绘制的Card不再在Deck之中的测试。
清单4:更多的测试
public void DeckShouldNotContainACardAfterDrawingIt()
{
var deck = new Deck(new List<Card> { new Card(), new Card(), new Card() });
Card card = deck.DrawCard();
Assert.IsNotNull(card);
Assert.IsFalse(deck.ContainsCard(card));
}
这个测试也将失败,因为我们还没有创建检测一张扑克牌是否还在Deck中的方法。现在我们需要添加该方法的代码来让这两个方法测试通过。
清单5:实现方法代码
{
return _cards.Contains(card);
}
我们已经写完了大部分代码,这是确保我们测试通过所需要的代码。通常每一次我会尽可能地少写测试,因此我能把注意力放到确保每个测试都能正确执行上面。这对于确保我在创建对象之间的交互代码时不会转移我的注意力。
下载源代码:http://brendan.enrick.com/files/downloads/TDDSample.zip
总结:
在测试驱动开发过程中,有人使用下面的短语:“红,绿,重构”。我们说这些是因为我们要下决心在我们的失败测试的基础之上去写代码。一旦我们的测试通过,我们就能进行重构而不用担心对功能造成影响。请记住,测试是为了维护你的代码,以及让你有机会去确定你的代码所想要的接口。
原文地址:http://aspalliance.com/1823_Beginning_Test_Driven_Development.all