多个期望/断言来验证测试结果

发布于 2024-11-29 20:04:58 字数 837 浏览 4 评论 0原文

我读过几本关于 TDD 和 BDD 的书和文章,认为应该避免在单个单元测试或规范中出现多个断言或期望。我也能理解这样做的原因。我仍然不确定验证复杂结果的好方法是什么。

假设被测试的方法返回一个复杂的对象作为结果(例如反序列化或数据库读取),我如何正确验证结果?

1. 对每个属性进行断言:

  Assert.AreEqual(result.Property1, 1);
  Assert.AreEqual(result.Property2, "2");
  Assert.AreEqual(result.Property3, null);
  Assert.AreEqual(result.Property4, 4.0);

2. 依赖于正确实现的 .Equals():

Assert.AreEqual(result, expectedResult);

1. 的缺点是,如果第一个断言失败,则所有后续断言都不会运行,这些断言可能包含查找问题的有价值的信息。随着属性的变化,可维护性也可能是一个问题。

2. 的缺点是我似乎用这个测试测试了不止一件事。如果 .Equals() 未正确实现,我可能会得到误报或误报。另外,对于 2,我不知道如果测试失败,哪些属性实际上会有所不同,但我认为通常可以通过适当的 .ToString() 覆盖来解决这个问题。无论如何,我认为我应该避免被迫在失败的测试中使用调试器来查看差异。我应该马上就看到它。

2. 的下一个问题是它比较整个对象,即使对于某些测试只有某些属性可能很重要。

在 TDD 和 BDD 中,什么是合适的方法或最佳实践?

I have read in several book and articles about TDD and BDD that one should avoid multiple assertions or expectations in a single unit test or specification. And I can understand the reasons for doing so. Still I am not sure what would be a good way to verify a complex result.

Assuming a method under test returns a complex object as a result (e.g. deserialization or database read) how do I verify the result correctly?

1.Asserting on each property:

  Assert.AreEqual(result.Property1, 1);
  Assert.AreEqual(result.Property2, "2");
  Assert.AreEqual(result.Property3, null);
  Assert.AreEqual(result.Property4, 4.0);

2.Relying on a correctly implemented .Equals():

Assert.AreEqual(result, expectedResult);

The disadvantage of 1. is that if the first assert fails all the following asserts are not run, which might have contained valuable information to find the problem. Maintainability might also be a problem as Properties come and go.

The disatvantage of 2. is that I seem to be testing more than one thing with this test. I might get false positives or negatives if .Equals() is not implemented correctly. Also with 2. I do not see, what properties are actually different if the test fails but I assume that can often be addressed with a decent .ToString() override. In any case I think I should avoid to be forced to throw the debugger at the failing tests to see the difference. I should see it right away.

The next problem with 2. is that it compares the whole object even though for some tests only some properties might be significant.

What would be a decent way or best practise for this in TDD and BDD.

如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。

评论(4

我是男神闪亮亮 2024-12-06 20:04:59

不要从字面上理解 TDD 建议。 “好人”的意思是,您应该在每个测试中测试一件事(以避免测试因多种原因失败并随后必须调试测试以查找原因)。
现在测试“一件事”意味着“一种行为”;恕我直言,每个测试都没有一个断言。

这是指导方针而不是规则。

所以选项:
用于比较整个数据值对象

  • 如果该对象已经公开了可用的产生式 Equals,请使用它。
  • 否则,不要仅为了测试而添加 Equals(另请参阅平等污染)。使用帮助器/扩展方法(或者您可以在断言库中找到一个) obj1.HasSamePropertiesAs(obj2)

为了比较对象的非结构化部分(任意属性集 - 这应该很少见),

  • 创建一个命名良好的私有方法 AssertCustomerDetailsInOrderEquals(params )以便测试清楚您实际测试的部分。将断言集移至私有方法中。

Don't take TDD advice literally. What the "good guys" mean is that you should test one thing per test (to avoid a test failing for multiple reasons and subsequently having to debug the test to find the cause).
Now test "one thing" means "one behavior" ; NOT one assert per test IMHO.

It's a guideline not a rule.

So options:
For comparing whole data value objects

  • If the object already exposes a usable production Equals, use it.
  • Else do not add a Equals just for testing (See also Equality Pollution). Use a helper/extension method (or you could find one in an assertion library) obj1.HasSamePropertiesAs(obj2)

For comparing unstructured parts of objects (set of arbitrary properties - which should be rare),

  • create a well named private method AssertCustomerDetailsInOrderEquals(params) so that the test is clear in what part you're actually testing. Move the set of assertions into the private method.
您的好友蓝忘机已上羡 2024-12-06 20:04:59

根据问题中存在的上下文,我会选择选项 1。

它可能取决于上下文。如果我在 .NET 框架内使用某种内置的对象序列化,我可以合理地确信,如果没有遇到错误,那么整个对象都会得到适当的封送。在这种情况下,断言对象中的单个字段可能就可以了。我相信 MS 库会做正确的事情。

如果您使用 SQL 并手动将结果映射到域对象,我认为选项 1 可以比选项 2 更快地诊断某些问题。选项 2 可能依赖于 toString 方法来呈现断言失败:

Expected <1 2 null 4.0> but was <1 2 null null>

现在我一直在试图弄清楚 4.0/null 是什么字段。当然,我可以将字段名称放入方法中:

Expected <Property1: 1, Property2: 2, Property3: null, Property4: 4.0>
 but was <Property1: 1, Property2: 2, Property3: null, Property4: null>

这对于少量属性来说很好,但由于包装等原因,开始分解大量属性。此外,toString 维护可能会变得这是一个问题,因为它需要以与 equals 方法相同的速率变化。

当然,没有正确的答案,归根结底,这实际上取决于您的团队(或您自己)的个人偏好。

希望有帮助!

布兰登

With the context present in the question I'd go for option 1.

It likely depends on context. If I'm using some sort of built in object serialization within the .NET framework, I can be reasonably assured that if no errors were encountered then the entire object was appropriately marshaled. In that case, asserting a single field in the object is probably fine. I trust MS libraries to do the right thing.

If you are using SQL and manually mapping results to domain objects I feel that option 1 makes it quicker to diagnose when something breaks than option 2. Option 2 likely relies on toString methods in order to render the assertion failure:

Expected <1 2 null 4.0> but was <1 2 null null>

Now I am stuck trying to figure out what field 4.0/null was. Of course I could put the field name into the method:

Expected <Property1: 1, Property2: 2, Property3: null, Property4: 4.0>
 but was <Property1: 1, Property2: 2, Property3: null, Property4: null>

This is fine for small numbers of properties, but begins to break down larger numbers of properties due to wrapping, etc. Also, the toString maintenance could become an issue as it needs to change at the same rate as the equals method.

Of course there is no correct answer, at the end of the day, it really boils down to your team's (or your own) personal preference.

Hope that helps!

Brandon

帅的被狗咬 2024-12-06 20:04:59

我会默认使用第二种方法。你是对的,如果 Equals() 未正确实现,则会失败,但如果您实现了自定义 Equals(),则也应该对其进行单元测试。

第二种方法实际上更加抽象和简洁,允许您以后更轻松地修改代码,从而以同样的方式减少代码重复。假设您选择第一种方法:

  • 您必须在多个位置比较所有属性,
  • 如果向类添加新属性,则必须在单元测试中添加新断言;修改您的 Equals() 会容易得多。当然,您仍然需要在预期结果中添加属性值(如果它不是默认值),但这比添加新断言要短。

此外,使用第二种方法更容易看出哪些属性实际上有所不同。您只需在调试模式下运行测试,并比较中断时的属性。

顺便说一句,您永远不应该为此使用 ToString()。我想你想说的是 [DebuggerDisplay] 属性?

2. 的下一个问题是它比较整个对象,即使对于某些测试来说只有某些属性可能很重要。

如果您只需比较某些属性,那么:

  • 您可以通过实现仅包含这些属性的基类来重构代码。示例:如果您想要将 Cat 与另一个 Cat 进行比较,但仅考虑 Dog 和其他动物的共同属性,请实现 Cat : Animal 并比较基类。
  • 或者您执行第一种方法中所做的操作。示例:如果您只关心预期的猫和实际的猫喝了多少牛奶以及它们各自的名字,那么您的单元测试中将有两个断言。

I would use the second approach by default. You're right, this fails if Equals() is not implemented correctly, but if you've implemented a custom Equals(), you should have unit-tested it too.

The second approach is in fact more abstract and consise and allows you to modify the code easier later, allowing in the same way to reduce code duplication. Let's say you choose the first approach:

  • You'll have to compare all properties in several places,
  • If you add a new property to a class, you'll have to add a new assertion in unit tests; modifying your Equals() would be much easier. Of course you have still to add a value of the property in expected result (if it's not the default value), but it would be shorter to do than adding a new assertion.

Also, it is much easier to see what properties are actually different with the second approach. You just run your tests in debug mode, and compare the properties on break.

By the way, you should never use ToString() for this. I suppose you wanted to say [DebuggerDisplay] attribute?

The next problem with 2. is that it compares the whole object even though for some tests only some properties might be significant.

If you have to compare only some properties, than:

  • ether you refactor your code by implementing a base class which contains only those properties. Example: if you want to compare a Cat to another Cat but only considering the properties common to Dog and other animals, implement Cat : Animal and compare the base class.
  • or you do what you've done in your first approach. Example: if you do care only about the quantity of milk the expected and the actual cats have drunk and their respective names, you'll have two assertions in your unit test.
好菇凉咱不稀罕他 2024-12-06 20:04:59

尝试“每个测试行为的一个方面”而不是“每个测试一个断言”。如果您需要多个断言来说明您感兴趣的行为,请这样做。

例如,您的示例可能是 ShouldHaveSensibleDefaults。将其拆分为 ShouldHaveADefaultNameAsEmptyStringShouldHaveNullAddressShouldHaveAQuantityOfZero 等不会读得很清楚。将合理的默认值隐藏在另一个对象中然后进行比较也无济于事。

但是,我会将具有默认值的示例与从某处的某些逻辑派生的任何属性分开,例如ShouldCalculateTheTotalQuantity。将这样的小示例移动到它们自己的方法中可以使其更具可读性。

您可能还会发现对象的不同属性会被不同的上下文更改。调出每个上下文并分别查看这些属性有助于我了解上下文与结果的关系。

Dave Astels 提出了“每个测试一个断言”,现在使用短语 “行为的一个方面”,尽管他仍然发现它对 将其分开行为。我倾向于在可读性和可维护性方面犯错误,所以如果有多个断言具有务实意义,我会这样做。

Try "one aspect of behavior per test" rather than "one assertion per test". If you need more than one assertion to illustrate the behavior you're interested in, do that.

For instance, your example might be ShouldHaveSensibleDefaults. Splitting that up into ShouldHaveADefaultNameAsEmptyString, ShouldHaveNullAddress, ShouldHaveAQuantityOfZero etc. won't read as clearly. Nor will it help to hide the sensible defaults in another object then do a comparison.

However, I would separate examples where the values had defaults with any properties derived from some logic somewhere, for instance, ShouldCalculateTheTotalQuantity. Moving small examples like this into their own method makes it more readable.

You might also find that different properties on your object are changed by different contexts. Calling out each of these contexts and looking at those properties separately helps me to see how the context relates to the outcome.

Dave Astels, who came up with the "one assertion per test", now uses the phrase "one aspect of behavior" too, though he still finds it useful to separate that behavior. I tend to err on the side of readability and maintainability, so if it makes pragmatic sense to have more than one assertion, I'll do that.

~没有更多了~
我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
原文