简体   繁体   English

如何在具有多个间接层的项目中使用单元测试

[英]How to use unit tests in projects with many levels of indirection

I was looking over a fairly modern project created with a big emphasis on unit testing. 我正在寻找一个相当现代的项目,重点是单元测试。 In accordance with old adage "every problem in object oriented programming can be solved by introducing new layer of indirection" this project was sporting multiple layers of indirection. 按照古老的格言“面向对象编程中的每个问题都可以通过引入新的间接层来解决”这个项目是多层间接的。 The side-effect was that fair amount of code looked like following: 副作用是相当数量的代码如下所示:

public bool IsOverdraft)
{
    balanceProvider.IsOverdraft();
}

Now, because of the empahsis on unit testing and maintaining high code coverage, every piece of code had unit tests written against it.Therefore this little method would have three unit tests present. 现在,由于单元测试的重点和维护高代码覆盖率,每一段代码都有针对它编写的单元测试。因此,这个小方法将有三个单元测试。 Those would check: 那些会检查:

  1. If balanceProvider.IsOverdraft() returns true then IsOverdraft should return true 如果balanceProvider.IsOverdraft()返回true,则IsOverdraft应返回true
  2. If balanceProvider.IsOverdraft() returns false then IsOverdraft should return false 如果balanceProvider.IsOverdraft()返回false,则IsOverdraft应返回false
  3. If balanceProvider throws an exception then IsOverdraft should rethrow the same exception 如果balanceProvider抛出异常,则IsOverdraft应重新抛出相同的异常

To make things worse, the mocking framework used (NMock2) accepted method names as string literals, as follows: 更糟糕的是,使用的模拟框架(NMock2)接受了方法名称作为字符串文字,如下所示:

NMock2.Expect.Once.On(mockBalanceProvider)
    .Method("IsOverdraft")
    .Will(NMock2.Return.Value(false));

That obviously made "red, green, refactor" rule into "red, green, refactor, rename in test, rename in test, rename in test". 这显然使“红色,绿色,重构”规则变为“红色,绿色,重构,重新测试,重新测试,重新测试”。 Using differnt mocking framework like Moq, would help with refactoring, but it would require a sweep trough all existing unit tests. 使用不同的模拟框架(如Moq)将有助于重构,但它需要扫描所有现有的单元测试。

What is the ideal way to handle this situation? 处理这种情况的理想方法是什么?

A) Keep smaller levels of layers, so that those forwarding calls do not happen anymore. A)保持较小级别的层,以便不再发生这些转发呼叫。

B) Do not test those forwarding methods, as they do not contain business logic. B)不要测试那些转发方法,因为它们不包含业务逻辑。 For purposes of coverage marked them all with ExcludeFromCodeCoverage attribute. 出于覆盖的目的,使用ExcludeFromCodeCoverage属性标记它们。

C) Test only if proper method is invoked, without checking return values, exceptions, etc. C)仅在调用适当的方法时进行测试,而不检查返回值,异常等。

D) Suck it up, and keep writing those tests ;) D)把它吸干,继续写那些测试;)

Either B or C. That's the problem with such general requirements ( "every method must have unit test, every line of code needs to be covered" ) - sometimes, benefit they provide is not worth the cost. 无论是B还是C.这就是这些一般要求的问题( “每种方法都必须进行单元测试,每行代码需要覆盖” ) - 有时,他们提供的好处不值得花费。 If it's something you came up with, I suggest rethinking this approach. 如果这是你想出的东西,我建议重新考虑这种方法。 The "we must have 95% code coverage" might be appealing on paper but in practice it quickly spawns problems like the one you have. “我们必须拥有95%的代码覆盖率”可能在纸面上具有吸引力,但在实践中它很快就会产生类似你所拥有的问题。

Also, the code you're testing is something I'd call trivial code . 此外,您正在测试的代码是我称之为琐碎的代码 Having 3 tests for it is most likely overkill. 对它进行3次测试很可能是矫枉过正。 For that single line of code, you'll have to maintain like 40 more. 对于那一行代码,你必须保持40多个。 Unless your software is mission critical (which might explain high-coverage requirement), I'd skip those tests. 除非您的软件是关键任务(这可能解释了高覆盖率要求),否则我会跳过这些测试。

One of the (IMHO) most pragmatic advices on this topic was provided by Kent Beck some time ago on this very site and I expanded a bit on those thoughts with in my blog posts - What should you test? 关于这个主题的(IMHO) 最实用的建议之一是肯特贝克不久前在这个网站上提供的,我在博客文章中对这些想法进行了一些扩展 - 你应该测试什么?

Honestly, I think we should write tests only to document our code in an helpful manner . 老实说,我认为我们应该编写测试,以便以有用的方式记录我们的代码 We should not write tests just for the sake of code coverage. 我们不应仅仅为了代码覆盖而编写测试。 (Code coverage is just a great tool to figure out what it is NOT covered so that we can figure out if we did forget important unit tests cases or if we actually have some dead code somewhere). (代码覆盖率只是一个很好的工具,可以找出它未被覆盖的内容,以便我们可以弄清楚我们是否忘记了重要的单元测试用例,或者我们是否真的在某处有一些死代码)。

If I write a test, but the test ends up just being a "duplication" of the implementation or worse...if it's harder to understand the test than the actual implementation ....then really such a test should not exists . 如果我写一个测试,但测试最终只是实现的“重复”或更糟糕...如果它比实际实现更难理解测试 ....那么真的这样的测试不应该存在 Nobody is interested in reading such tests. 没有人有兴趣阅读这样的测试。 Tests should not contain implementation details . 测试不应包含实现细节 Test are about "what" should happen not "how" it will be done. 测试是关于应该发生什么”而不是“将如何”完成。 Since you've tagged your question with "TDD", I would add that TDD is a design practice. 既然你用“TDD”标记了你的问题,我想补充说TDD是一种设计实践。 So if I already know 100% sure in advance what will be the design of what i'm going to implement, then there is no point for me to use TDD and write unit tests ( But I will always have in all cases a high level acceptance test that will cover that code ). 因此,如果我已经提前100%确定将要实现的设计,那么我就没有必要使用TDD和编写单元测试( 但我总是在所有情况下都有高水平接受测试将涵盖该代码 )。 That will happen often when the thing to design is really simple, like in your example. 当设计的东西非常简单时(例如在你的例子中),这种情况经常发生。 TDD is not about testing and code coverage, but really about helping us to design our code and document our code. TDD不是关于测试和代码覆盖,而是关于帮助我们设计代码和记录代码。 There is no point to use a design tool or a documentation tool for designing/documenting simple/obvious things. 没有必要使用设计工具或文档工具来设计/记录简单/明显的东西。

In your example, it's far easier to understand what's going on by reading directly the implementation than the test. 在您的示例中,通过直接读取实现而不是测试来了解正在发生的事情要容易得多。 The test doesn't add any value in term of documentation. 测试不会在文档方面增加任何价值。 So I'd happily erase it. 所以我很高兴擦掉它。

On top of that such tests are horridly brittle , because they are tightly coupled to the implementation. 最重要的是, 这些测试非常脆弱 ,因为它们与实现紧密耦合 That's a nightmare on the long term when you need to refactor stuff since any time you will want to change the implementation they will break. 从长远来看,当你需要重构东西时,这是一场噩梦,因为任何时候你都想要改变它们将会破坏的实现。

What I'd suggest to do, is to not write such tests but instead have higher level component tests or fast integration tests/acceptance tests that would exercise these layers without knowing anything at all about the inner working. 我建议做的是,不要编写这样的测试,而是进行更高级别的组件测试或快速集成测试/验收测试,这些测试将在不了解内部工作的情况下运用这些层。

I think one of the most important things to keep in mind with unit tests is that it doesn't necessarily matter how the code is implemented today, but rather what happens when the tested code, direct or indirect, is modified in the future. 我认为单元测试要记住的最重要的事情之一是,它不一定与今天的代码实现方式有关,而是在将来修改直接或间接测试代码时会发生什么。

If you ignore those methods today and they are critical to your application's operation, then someone decides to implement a new balanceProvider at some point down the road or decides that the redirection no longer makes sense, you will most likely have a failure point. 如果您今天忽略这些方法并且它们对您的应用程序的操作至关重要,那么有人决定在未来的某个时刻实施新的balanceProvider或者决定重定向不再有意义,您很可能会遇到故障点。

So, if this were my application, I would first look to reduce the forward-only calls to a bare minimum (reducing the code complexity), then introduce a mocking framework that does not rely on string values for method names. 所以,如果这是我的应用程序,我首先会考虑将仅向前调用减少到最低限度(降低代码复杂性),然后引入一个不依赖于方法名称的字符串值的模拟框架。

A couple of things to add to the discussion here. 这里有一些要添加到讨论中的东西。

Switch to a better mocking framework immediately and incrementally. 立即并逐步切换到更好的模拟框架。 We switched from RhinoMock to Moq about 3 years ago. 大约3年前,我们从RhinoMock切换到Moq。 All new tests used Moq, and often when we change a test class we switch it over. 所有新测试都使用Moq,通常当我们更改测试类时,我们会将其切换。 But areas of the code that haven't changed much or have huge test casses are still using RhinoMock and that is OK. 但是代码中没有太大变化或者有大量测试成本的区域仍在使用RhinoMock,这没关系。 The code we work with from day to day is much better as a result of making the switch. 由于进行切换,我们日常工作的代码要好得多。 All test changes can happen in this incremental way. 所有测试更改都可以以此增量方式进行。

You are writing too many tests. 你写了太多的考试。 An important thing to keep in mind in TDD is that you should only write code to satisfy a red test, and you should only write a test to specify some unwritten code. 在TDD中要记住的一件重要事情是,您应该只编写代码以满足红色测试,并且您应该只编写一个测试来指定一些未编写的代码。 So in your example, three tests is overkill, because at most two are needed to force you to write all of that production code. 因此,在您的示例中,三个测试是过度的,因为最多需要两个测试来强制您编写所有生产代码。 The exception test does not make you write any new code, so there is no need to write it. 异常测试不会让您编写任何新代码,因此无需编写代码。 I would probably only write this test: 我可能只会写这个测试:

[Test]
public void IsOverdraftDelegatesToBalanceProvider()
{
    var result = RandomBool();
    providerMock.Setup(p=>p.IsOverdraft()).Returns(result);
    Assert.That(myObject.IsOverDraft(), Is.EqualTo(result);
}

Don't create useless layers of indirection. 不要创建无用的间接层。 Mostly, unit tests will tell you if you need indirection. 大多数情况下,单元测试会告诉您是否需要间接。 Most indirection needs can be solved by the dependency inversion principle, or "couple to abstractions, not concretions". 大多数间接需求可以通过依赖性倒置原则来解决,或者“耦合到抽象,而不是结果”。 Some layers are needed for other reasons (I make WCF ServiceContract implementations a thin pass through layer. I also don't test that pass through). 由于其他原因需要一些层(我使WCF ServiceContract实现成为薄层传递层。我也不测试该传递)。 If you see a useless layer of indirection, 1) make sure it really is useless, then 2) delete it. 如果你看到一个无用的间接层,1)确保它真的没用,那么2)删除它。 Code clutter has a huge cost over time. 随着时间的推移,代码混乱会带来巨大的成本。 Resharper makes this ridiculously easy and safe. Resharper使这个过于简单和安全。

Also, for meaningful delegation or delegation scenarios you can't get rid of but need to test, something like this makes it a lot easier. 此外,对于有意义的委派或委派方案,你无法摆脱但需要测试, 这样的事情使它变得容易多了。

I'd say D) Suck it up, and keep writing those tests ;) and try to see if you can replace NMock with MOQ. 我会说D)把它吸干,并继续编写这些测试;)并试着看看你是否可以用MOQ替换NMock。

It might not seem necessary and even though it's just delegation now, but the tests are testing that it's calling the right method with right parameters, and the method itself is not doing anything funky before returning values. 它似乎没有必要,即使它现在只是委托,但测试正在测试它正在使用正确的参数调用正确的方法,并且方法本身在返回值之前没有做任何时髦的事情。 So it's a good idea to cover them in tests. 所以在测试中覆盖它们是个好主意。 But to make it easier use MOQ or similiar framework that'll make it so much easier to refactor. 但是为了更容易使用MOQ或类似的框架,这将使它更容易重构。

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

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