6

我正在抽象我的一个类的历史跟踪部分,使其看起来像这样:

private readonly Stack<MyObject> _pastHistory = new Stack<MyObject>();

internal virtual Boolean IsAnyHistory { get { return _pastHistory.Any(); } }

internal virtual void AddObjectToHistory(MyObject myObject)
{
  if (myObject == null) throw new ArgumentNullException("myObject");
  _pastHistory.Push(myObject);
}

internal virtual MyObject RemoveLastObject()
{
  if(!IsAnyHistory) throw new InvalidOperationException("There is no previous history.");
  return _pastHistory.Pop();
}

我的问题是我想单元测试 Remove 将返回最后添加的对象。

  • AddObjectToHistory
  • RemoveObjectToHistory-> 返回通过AddObjectToHistory

但是,如果我必须先调用 Add ,这不是真正的单元测试吗?但是,我可以看到以真正的单元测试方式执行此操作的唯一方法是在构造函数中传入 Stack 对象或模拟出来IsAnyHistory......但模拟我的 SUT 也很奇怪。所以,我的问题是,从教条的角度来看,这是一个单元测试吗?如果没有,我该如何清理它......构造函数注入是我唯一的方法吗?必须传入一个简单的对象似乎有点牵强?甚至可以将这个简单的对象推出以进行注入吗?

4

5 回答 5

4

这些场景有两种方法:

  1. 干扰设计,例如制作_pastHistory internal/protected或注入堆栈
  2. 使用其他(可能是单元测试的)方法来执行验证

与往常一样,没有黄金法则,尽管我会说您通常应该避免单元测试强制更改设计的情况(因为这些更改很可能会给代码使用者带来歧义/不必要的问题)。

尽管如此,最终还是你必须权衡你希望单元测试代码在多大程度上干扰设计(第一种情况)或弯曲完美的单元测试定义(第二种情况)。

通常,我发现第二种情况更有吸引力——它不会弄乱原始类代码,而且你很可能Add已经测试过了——依赖它是安全的

于 2013-06-24T19:43:36.003 回答
1

我认为它仍然是一个单元测试,假设 MyObject 是一个简单的对象。我经常为单元测试方法构造输入参数。

我使用Michael Feather 的单元测试标准

如果满足以下条件,则测试不是单元测试:

  • 它与数据库对话
  • 它通过网络进行通信
  • 它涉及文件系统
  • 它不能与您的任何其他单元测试同时运行
  • 你必须对你的环境做一些特殊的事情(比如编辑配置文件)来运行它。

做这些事情的测试还不错。它们通常值得编写,并且可以在单元测试工具中编写。但是,能够将它们与真正的单元测试分开是很重要的,这样我们就可以保留一组测试,以便在我们进行更改时可以快速运行。

于 2013-06-24T19:17:33.003 回答
1

我的 2 美分...客户如何知道删除是否有效?“客户”应该如何与该对象进行交互?客户是否会将堆栈推送到历史跟踪器?将测试视为测试对象的另一个用户/消费者/客户......使用与实际生产中完全相同的交互。我还没有听说过任何规定不允许在被测对象上调用多个方法的规则。

为了模拟,堆栈不为空。我只会打电话给 Add - 99% 的情况。我会避免破坏该对象的封装。像人一样对待对象(我想我在对象思维中读到过)。告诉他们做事..不要闯入并进入。

例如,如果你想让某人的钱包里有一些钱,

  • 最简单的方法是给他们钱,让他们在内部把钱放进钱包。
  • 把他们的钱包扔掉,然后塞进他们口袋里的钱包里。

我喜欢选项1。另请参阅它如何将您从实现细节中解放出来(这会导致测试变得脆弱)。假设明天这个人决定使用在线钱包。后一种方法会破坏你的测试——它们现在需要更新才能推入在线钱包——即使对象行为没有被破坏。

我见过的另一个例子是测试 Repository.GetX() ,人们现在在单元测试中闯入数据库以使用 SQL 注入记录。调用 Repository.AddX(x ) 第一的。孤立是可取的,但不能超越实用主义。

我希望我在这里没有表现得太强.. 看到对象 API 被“为了可测试性而扭曲”到不再类似于“可以工作的最简单的东西”的地步,这让我很痛苦。

于 2013-06-25T05:13:18.880 回答
0

正如其他答案中所述,我认为您的选择可以这样分解。

  1. 您对测试方法采用教条的方法,并为堆栈对象添加构造函数注入,以便您可以注入自己的假堆栈对象并测试您的方法。

  2. 您为添加和删除编写单独的测试,删除测试将使用 add 方法,但将其视为测试设置的一部分。只要您的 add 测试通过,您的 remove 也应该通过。

于 2013-06-24T20:50:52.470 回答
0

我认为您试图对单元测试的定义过于具体。您应该测试班级的公共行为,而不是详细的实施细节。

从您的代码片段看来,您真正需要关心的是 a) 调用 AddObjectToHistory 是否会导致 IsAnyHistory 返回 true,b) RemoveLastObject 最终会导致 IsAnyHistory 返回 false。

于 2013-06-24T19:41:18.817 回答