【问题标题】:Implementing a Stack using Test-Driven Development使用测试驱动开发实现堆栈
【发布时间】:2010-05-23 21:16:08
【问题描述】:

我正在使用 TDD 迈出第一步。问题是(可能每个人都是从 TDD 开始的),当我开始在我的项目中工作时,我永远不知道该做什么样的单元测试。

假设我想用以下方法编写一个 Stack 类(我选择它是因为它是一个简单的例子):

Stack<T>
 - Push(element : T)
 - Pop() : T
 - Peek() : T
 - Count : int
 - IsEmpty : boolean

你会如何处理这个?我从来不明白这个想法是要为 Stack 类的每个方法测试一些极端案例,还是先对类做一些“用例”,比如添加 10 个元素并删除它们。想法是什么?使使用堆栈的代码尽可能接近我将在实际代码中使用的代码?或者只是进行简单的“添加一个元素”单元测试,测试 IsEmpty 和 Count 是否通过添加该元素而改变?

我应该如何开始?

编辑

这是我的粗略测试的实现:

    [TestMethod]
    public void PushTests() {
        StackZ<string> stackz = new StackZ<string>();

        for (int i = 0; i < 5; ++i) {
            int oldSize = stackz.Size;
            stackz.Push(i.ToString());
            int newSize = stackz.Size;
            Assert.AreEqual(oldSize + 1, newSize);
            Assert.IsFalse(stackz.IsEmpty);
        }
    }

    [TestMethod, ExpectedException(typeof(InvalidOperationException))]
    public void PeekTestsWhenEmpty() {
        StackZ<double> stackz = new StackZ<double>();
        stackz.Peek();
    }

    [TestMethod]
    public void PeekTestsWhenNotEmpty() {
        StackZ<int> stackz = new StackZ<int>();
        stackz.Push(5);

        int firstPeekValue = stackz.Peek();

        for (int i = 0; i < 5; ++i) {
            Assert.AreEqual(stackz.Peek(), firstPeekValue);
        }
    }

    [TestMethod, ExpectedException(typeof(InvalidOperationException))]
    public void PopTestsWhenEmpty() {
        StackZ<float> stackz = new StackZ<float>();
        stackz.Pop();
    }

    [TestMethod]
    public void PopTestsWhenNotEmpty() {
        StackZ<int> stackz = new StackZ<int>();

        for (int i = 0; i < 5; ++i) {
            stackz.Push(i);
        }

        for (int i = 4; i >= 0; ++i) {
            int oldSize = stackz.Size;
            int popValue = stackz.Pop();
            Assert.AreEqual(popValue, i);
            int newSize = stackz.Size;
            Assert.AreEqual(oldSize, newSize + 1);
        }

        Assert.IsTrue(stackz.IsEmpty);
    }

关于它的任何更正/想法?谢谢

【问题讨论】:

  • 除了你的具体问题,我想推荐马丁福勒关于重构的书籍和文章,他建议在重构之前实施测试,同样的想法在实施之前有效,例如refactoring.com
  • 我一直在阅读他的重构书的部分内容。无论如何你:)

标签: c# java unit-testing tdd


【解决方案1】:

从测试 API 的基本原则开始。

测试零元素。

  • 测试它是否为空。
  • 计数为零。
  • 弹出失败。

测试一个元素:

  • 呼叫推送。
  • 测试它不为空。
  • 测试计数为 1。
  • 测试 Pop 返回元素。
  • 测试它现在是空的。
  • 测试计数现在为 0。

测试 >1 个元素:

  • 现在推送 2,测试计数为 2。
  • Pop 2 并确保它们按 LIFO 顺序排列。
  • 检查空和计数。

每一个都至少是一个测试用例。

例如(在 Google 的 c++ 单元测试框架中粗略概述):

TEST(StackTest, TestEmpty) {
  Stack s;
  EXPECT_TRUE(s.empty());
  s.push(1);
  EXPECT_FALSE(s.empty());
  s.pop();
  EXPECT_TRUE(s.empty());
}

TEST(StackTest, TestCount) {
  Stack s;
  EXPECT_EQ(0, s.count());
  s.push(1);
  EXPECT_EQ(1, s.count());
  s.push(2);
  EXPECT_EQ(2, s.count());
  s.pop();
  EXPECT_EQ(1, s.count());
  s.pop();
  EXPECT_EQ(0, s.count());
}

TEST(StackTest, TestOneElement) {
  Stack s;
  s.push(1);
  EXPECT_EQ(1, s.pop());
}

TEST(StackTest, TestTwoElementsAreLifo) {
  Stack s;
  s.push(1);
  s.push(2);
  EXPECT_EQ(2, s.pop());
  EXPECT_EQ(1, s.pop());
}

TEST(StackTest, TestEmptyPop) {
  Stack s;
  EXPECT_EQ(NULL, s.pop());
}


TEST(StackTest, TestEmptyOnEmptyPop) {
 Stack s;
  EXPECT_TRUE(s.empty());
  s.pop();
  EXPECT_TRUE(s.empty());
}

TEST(StackTest, TestCountOnEmptyPop) {
  Stack s;
  EXPECT_EQ(0, s.count());
  s.pop();
  EXPECT_EQ(0, s.count());
}

【讨论】:

  • 我应该进行 3 次测试还是应该针对每个项目符号项进行一次测试?
  • 吞噬:当引入一个错误时,您更喜欢“测试一个元素失败”或“推送一次后,计数不是 1”之类的消息吗?如果是前者,则使用三个测试,如果是后者,则对每个要点进行一个测试。 (我知道我会选择什么:)
  • 这只是为了确保 :P 顺便说一句,您会将 3 个案例中的每一个都放在不同的测试类中吗?
  • 当您说“测试计数为 1”(例如)时 - 这是一个不同的测试还是只是一个断言?
  • 通过这种类型的测试,很难知道您是否真的涵盖了所有基础。如果出现新要求,您会更改哪个测试以确保测试新功能?如果你所有的测试都只是用例,那么就很难知道你是否真的测试了所有东西。您可能会多次测试某些东西,而另一些则根本不会。对于 Stack 来说,这是可管理的,对于更复杂的 Classes,它将变得难以管理。除了用例测试之外,您还可以进行测试个别需求的测试。
【解决方案2】:

如果你更详细地写出每个方法的要求,这将为你提供更多关于你需要的单元测试的提示。然后,您可以编写这些测试。如果你有一个自动完成的 IDE,比如 IDEA,那么做 TDD 很简单,因为它强调了你还没有实现的所有位。

例如,如果要求是“pop() on an empty stack throws a NoSuchElementException”,那么您可以从

@Test(exception=NoSuchElementException.class)
void popOnEmptyStackThrowsException()
{
   Stack s = new Stack();
   s.pop();
}

然后,IDE 将提示您如何处理缺少的 Stack 类。选项之一是“创建类”,因此您可以创建类。然后它询问您也选择创建的 pop 方法。现在,你可以实现你的 pop 方法,放入你需要的东西来实现合约。即

T pop() {
   if (size==0) throw new NoSuchElementException();
}

您继续,以这种方式迭代,直到您为所有堆栈要求实现了测试。和以前一样,IDE 会抱怨没有“大小”变量。在您创建测试用例“新创建的堆栈为空”之前,我将保留它,然后您可以在其中创建变量,因为它的初始化在该测试中得到验证。

处理完您的方法要求后,您可以添加一些更复杂的用例。 (理想情况下,这些用例将被指定为类级别的需求。)

【讨论】:

  • 您提出了一个很好的观点,即更简洁地了解方法的要求/合同。也许这是我的问题之一,因为我在编码之前往往从不考虑它们。
  • 还有一件事。在您的 pop 实现示例中,您将只制作“if (size == 0) throw blablabla”,还是您已经开始编写 pop() 方法逻辑的其余部分?我是否应该只在对 Stack 类进行单元测试后才实现它?
  • 嗯,TDD 的教义说,应该在编写代码之前编写测试,而且最好至少一开始就非常严格,这样你就养成了这个习惯。就个人而言,我确实倾向于使事情变得平滑,例如对于流行音乐,我至少可以勾勒出一个实现,同时存根相应的测试。重要的是最终得到满足所有规定要求的测试。
  • 新手问题:TDD 似乎提倡编写通过测试所需的最低要求。因此,对于第一次测试,“if (size==0)”真的有必要吗?只需“抛出新的 NoSuchElementException()”就足以通过测试。然后,理论上,您稍后会添加另一个在推送后弹出的测试,该测试将失败,您将添加“if (size==0)”。 TDD 是不是太过分了?
【解决方案3】:

我会这样开始:

  • create() - IsEmpty() == true -> 好的
  • 2x push() - count() == 2 -> 好的
  • peek() - T == 预期(最后推送)-> 好的(peek 假设 seek 是一个错字)
  • 2x pop() - count() == 0 && isEmppty -> OK

【讨论】:

  • 被这里的回复惊呆了,我想补充一点,实现和测试代码应该是平衡的,至少如果它与生活或其他非常关键的事情无关。
【解决方案4】:

理想情况下,测试必须涵盖类的所有功能。他们应该检查每个操作是否按照其合同行事。从理论上讲,我将合约视为 之间的映射。 因此,在设计测试之前,你应该定义好所有操作的契约。

以下是上述堆栈 API 的一些示例测试:

1) Push 应该将 Count() 返回的值增加 1

2) 在空堆栈上弹出应该抛出异常

3) Pop 应该将 Count() 返回的值减少 1

4) 推送 x1,x2,...,xn 然后弹出它们必须以相反的顺序返回它们 xn,...,x1

5) 添加元素,验证 isEmpty()==false 然后全部弹出并验证 isEmpty ()==真

6) Seek() 不能改变 Count() 返回的值

7) 对 Seek() 的连续调用必须返回相同的值 等等……

【讨论】:

    【解决方案5】:

    如果您阅读了the book Kent Beck 关于测试驱动开发的文章,您可能已经注意到书中经常出现的一个想法:您应该针对您目前缺少的内容编写测试。只要你不需要什么东西,就不要写测试也不要实现它。

    虽然 Stack 类的实现符合您的需求,但您不需要彻底实现它。在后台,它甚至可以向您返回常量或什么也不做。

    测试不应该成为您开发的开销,它应该加速您的开发,当您不想把所有事情都记在脑子里时,它会为您提供支持。

    TDD 的主要优势在于它使您可以编写可在一小行代码中测试的代码,因为通常您不想编写 50 行代码来测试一个方法。您会更加关心类之间的接口和功能分布,因为再一次,您不想编写 50 行代码来测试一个方法。

    话虽如此,我可以告诉你,通过对 superutil 接口实施单元测试来学习 TDD 并不有趣,并且可能有用,这些接口是通过几代开发人员的痛苦获得的。你不会感到任何令人兴奋的事情。只需从您编写的应用程序中获取任何类并尝试为其编写测试。重构它们会给你带来很多乐趣。

    【讨论】:

    • 我一直在阅读您推荐的书的第一个示例。我没有想到首先在乘法方法中返回 5 而不是从一开始就使用正确的。有什么意义?
    • @devoured elysium:关键是返回调用代码所期望的。当预期结果不同时,您的代码就会出错。从一开始并不总是清楚什么是正确的。即使可以,也不总是需要实现绝对正确性。乘法的例子只是一个例子。
    猜你喜欢
    • 1970-01-01
    • 2011-03-13
    • 2011-09-09
    • 2012-10-10
    • 1970-01-01
    • 1970-01-01
    • 2011-02-11
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多