简体   繁体   English

单元测试跟踪状态的类

[英]Unit testing a class that tracks state

I am abstracting the history tracking portion of a class of mine so that it looks like this: 我正在抽象一类我的历史跟踪部分,所以它看起来像这样:

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();
}

My problem is that I would like to unit test that Remove will return the last Added object. 我的问题是我想单元测试Remove会返回最后添加的对象。

  • AddObjectToHistory
  • RemoveObjectToHistory -> returns what was put in via AddObjectToHistory RemoveObjectToHistory - >返回通过AddObjectToHistory放入的内容

However, it isn't really a unit test if I have to call Add first? 但是,如果我必须先调用Add,它不是真正的单元测试吗? But, the only way that I can see to do this in a true unit test way is to pass in the Stack object in the constructor OR mock out IsAnyHistory ...but mocking my SUT is odd also. 但是,我能看到以真正的单元测试方式执行此操作的唯一方法是在构造函数中传入Stack对象或模拟出IsAnyHistory ......但是模拟我的SUT也很奇怪。 So, my question is, from a dogmatic view is this a unit test? 所以,我的问题是,从教条的角度来看,这是一个单元测试吗? If not, how do I clean it up...is constructor injection my only way? 如果没有,我该如何清理它...是构造函数注入我唯一的方法? It just seems like a stretch to have to pass in a simple object? 它似乎只是需要传递一个简单的对象? Is it ok to push even this simple object out to be injected? 是否可以将这个简单的物体推出注入?

There are two approaches to those scenarios: 这些方案有两种方法:

  1. Interfere into design, like making _pastHistory internal / protected or injecting stack 干扰设计,比如使_pastHistory internal / protected或注入堆栈
  2. Use other (possibly unit tested) methods to perform verification 使用其他(可能是单元测试的)方法来执行验证

As always, there is no golden rule, although I'd say you generally should avoid situations where unit tests force design changes (as those changes will most likely introduce ambiguity/unnecessary questions to code consumers). 与往常一样,没有黄金法则,虽然我会说你通常应该避免单元测试强制设计变更的情况(因为这些变化很可能会给代码消费者带来歧义/不必要的问题)。

Nonetheless, in the end it is you who has to weigh how much you want unit test code interfere into design (first case) or bend the perfect unit test definition (second case). 尽管如此,最终你需要权衡单元测试代码对设计的干扰程度(第一种情况)或弯曲完美的单元测试定义(第二种情况)。

Usually, I find second case much more appealing - it doesn't clutter original class code and you'll most likely have Add already tested - it's safe to rely on it . 通常情况下,我发现第二种情况更具吸引力 - 它不会使原始类代码混乱,并且您很可能已经测试了Add - 依赖它是安全的

I think it's still a unit test, assuming MyObject is a simple object. 假设MyObject是一个简单的对象,我认为它仍然是一个单元测试。 I often construct input parameters to unit test methods. 我经常为单元测试方法构造输入参数。

I use Michael Feather's unit test criteria : 我使用Michael Feather的单元测试标准

A test is not a unit test if: 在以下情况下,测试不是单元测试:

  • It talks to the database 它与数据库进行对话
  • It communicates across the network 它通过网络进行通信
  • It touches the file system 它触及文件系统
  • It can't run at the same time as any of your other unit tests 它不能与任何其他单元测试同时运行
  • You have to do special things to your environment (such as editing config files) to run it. 您必须对您的环境执行特殊操作(例如编辑配置文件)才能运行它。

Tests that do these things aren't bad. 做这些事情的测试也不错。 Often they are worth writing, and they can be written in a unit test harness. 通常它们值得写作,它们可以用单元测试工具编写。 However, it is important to be able to separate them from true unit tests so that we can keep a set of tests that we can run fast whenever we make our changes. 但是,能够将它们与真正的单元测试分开是很重要的,这样我们就可以保留一组测试,每当我们进行更改时,我们都可以快速运行这些测试。

My 2 cents... how would the client know if remove worked or not ? 我的2美分......客户如何知道删除是否有效? How is a 'client' supposed to interact with this object? “客户”应该如何与此对象进行交互? Are clients going to push in a stack to the history tracker? 客户是否会将堆栈推入历史跟踪器? Treat the test as just another user/consumer/client of the test subject.. using exactly the same interaction as in real production. 将测试视为测试对象的另一个用户/消费者/客户端...使用与实际生产中完全相同的交互。 I haven't heard of any rule stating that you're not allowed to call multiple methods on the object under test. 我没有听说过任何规则,说明你不允许在被测对象上调用多个方法。

To simulate, stack is not empty. 要模拟,堆栈不是空的。 I'd just call Add - 99% case. 我只是打电话给Add - 99%案例。 I'd refrain from destroying the encapsulation of that object.. Treat objects like people (I think I read that in Object Thinking). 我不会破坏那个对象的封装。像人一样对待对象(我想我在对象思维中读过这个)。 Tell them to do stuff.. don't break-in and enter. 告诉他们要做的事情......不要闯入并进入。

eg If you want someone to have some money in their wallet, 例如,如果你想让别人的钱包里有钱,

  • the simple way is to give them the money and let them internally put it into their wallet. 简单的方法是给他们钱,让他们内部把钱放进他们的钱包里。
  • throw their wallet away and stuff in a wallet in their pocket. 扔掉他们的钱包,塞进口袋里的钱包里。

I like Option1. 我喜欢Option1。 Also see how it frees you from implementation details (which induce brittleness in tests). 另请参阅它如何将您从实现细节中解放出来(这会导致测试中的脆弱性)。 Let's say tomorrow the person decides to use an online wallet. 让我们说明天该人决定使用在线钱包。 The latter approach will break your tests - they will need to be updated for pushing in an online wallet now - even though the object behavior is not broken. 后一种方法将破坏您的测试 - 他们将需要更新以便现在推送在线钱包 - 即使对象行为没有被破坏。

Another example I've seen is for testing Repository.GetX() where people break-in to the DB to inject records with SQL now in the unit test.. where it would have be considerably cleaner and easier to call Repository.AddX(x) first. 我看到的另一个例子是测试Repository.GetX(),其中人们闯入数据库以在单元测试中注入带有SQL的记录..在那里它将更清晰,更容易调用Repository.AddX(x )首先。 Isolation is desired but not to the extent that it overrides pragmatism. 需要隔离,但不能超越实用主义。

I hope I didn't come on too strong here.. it just pains me to see object APIs being 'contorted for testability' to the point where it no longer resembles the 'simplest thing that could work'. 我希望我在这里不会变得过于强大......我只是觉得对象API被“扭曲为可测试性”,以至于它不再像“可以工作的最简单的东西”。

I think you're trying to be a little overly specific with your definition of a unit test. 我认为你试图对单元测试的定义过于具体。 You should be testing the public behavior of your class, not the minute implementation details. 您应该测试类的公共行为,而不是分钟实现细节。

From your code snippet, it looks like all you really need to care about is whether a) calling AddObjectToHistory causes IsAnyHistory to return true and b) RemoveLastObject eventually causes IsAnyHistory to return false. 从你的代码片段看,你真正需要关心的是a)调用AddObjectToHistory是否导致IsAnyHistory返回true和b)RemoveLastObject最终导致IsAnyHistory返回false。

As stated in the other answers I think your options can be broken down like so. 正如其他答案所述,我认为你的选择可以这样分解。

  1. You take a dogmatic approach to your testing methodology and add constructor injection for the stack object so you can inject your own fake stack object and test your methods. 您对测试方法采用教条方法并为堆栈对象添加构造函数注入,以便您可以注入自己的假堆栈对象并测试方法。

  2. You write a separate test for add and remove, the remove test will use the add method but consider it a part of the test setup. 您为add和remove编写了单独的测试,remove测试将使用add方法,但将其视为测试设置的一部分。 As long as your add test passes, your remove should be too. 只要您的添加测试通过,您的删除也应该是。

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM