在处理 List时重构单元测试以使其可维护和可读的挑战物体
在单元测试的艺术一书中,它谈到了想要创建可维护和可读的单元测试。在第 204 页左右,它提到应该尽量避免在一个测试中使用多个断言,并且也许可以将对象与重写的 Equals 方法进行比较。当我们只有一个对象来比较预期结果与实际结果时,这非常有效。但是,如果我们有一个所述对象的列表(或集合)怎么办?
考虑下面的测试。我有不止一个断言。事实上,有两个独立的循环调用断言。在这种情况下,我最终会得到 5 个断言。 2 检查一个列表的内容是否存在于另一个列表中,2 反之亦然。第 5 个比较列表中元素的数量。
如果有人有改进这个测试的建议,我会洗耳恭听。我目前正在使用 MSTest,尽管我将 MSTest 的 Assert 替换为 NUnits 以实现流畅的 API (Assert.That)。
当前重构代码:
[TestMethod]
#if !NUNIT
[HostType("Moles")]
#else
[Moled]
#endif
public void LoadCSVBillOfMaterials_WithCorrectCSVFile_ReturnsListOfCSVBillOfMaterialsThatMatchesInput()
{
//arrange object(s)
var filePath = "Path Does Not Matter Because of Mole in File object";
string[] csvDataCorrectlyFormatted = { "1000, 1, Alt 1, , TBD, 1, 10.0, Notes, , Description, 2.50, ,A",
"1001, 1, Alt 2, , TBD, 1, 10.0, Notes, , Description, 2.50, ,A" };
var materialsExpected = new List<CSVMaterial>();
materialsExpected.Add(new CSVMaterial("1000", 1, "Alt 1", "TBD", 1m, 10.0m, "Notes", "Description", 2.50m,"A"));
materialsExpected.Add(new CSVMaterial("1001", 1, "Alt 2", "TBD", 1m, 10.0m, "Notes", "Description", 2.50m,"A"));
//by-pass actually hitting the file system and use in-memory representation of CSV file
MFile.ReadAllLinesString = s => csvDataCorrectlyFormatted;
//act on object(s)
var materialsActual = modCSVImport.LoadCSVBillOfMaterials(filePath);
//assert something happended
Assert.That(materialsActual.Count,Is.EqualTo(materialsExpected.Count));
materialsExpected.ForEach((anExpectedMaterial) => Assert.That(materialsActual.Contains(anExpectedMaterial)));
materialsActual.ForEach((anActualMaterial) => Assert.That(materialsExpected.Contains(anActualMaterial)));
}
原始多断言单元测试:
...
//1st element
Assert.AreEqual("1000", materials[0].PartNumber);
Assert.AreEqual(1, materials[0].SequentialItemNumber);
Assert.AreEqual("Alt 1", materials[0].AltPartNumber);
Assert.AreEqual("TBD", materials[0].VendorCode);
Assert.AreEqual(1m, materials[0].Quantity);
Assert.AreEqual(10.0m, materials[0].PartWeight);
Assert.AreEqual("Notes", materials[0].PartNotes);
Assert.AreEqual("Description", materials[0].PartDescription);
Assert.AreEqual(2.50m, materials[0].UnitCost);
Assert.AreEqual("A", materials[1].Revision);
//2nd element
Assert.AreEqual("1001", materials[1].PartNumber);
Assert.AreEqual(1, materials[1].SequentialItemNumber);
Assert.AreEqual("Alt 2", materials[1].AltPartNumber);
Assert.AreEqual("TBD", materials[1].VendorCode);
Assert.AreEqual(1m, materials[1].Quantity);
Assert.AreEqual(10.0m, materials[1].PartWeight);
Assert.AreEqual("Notes", materials[1].PartNotes);
Assert.AreEqual("Description", materials[1].PartDescription);
Assert.AreEqual(2.50m, materials[1].UnitCost);
Assert.AreEqual("A", materials[1].Revision);
}
In the book The Art of Unit Testing it talks about wanting to create maintainable and readable unit tests. Around page 204 it mentions that one should try to avoid multiple asserts in one test and, perhaps compare objects with an overridden Equals method. This works great when we have only one object to compare the expected vs. actual results. However what if we have a list (or collection) of said objects.
Consider the test below. I have more than one assert. In fact, there are two separate loops calling asserts. In this case I will end up with 5 assertions. 2 to check the contents of one list exist in another, and 2 vice versa. The 5th comparing the number of elements in the lists.
If anyone has suggestions to improve this test, I'm all ears. I am using MSTest at the moment, though I replaced MSTest's Assert with NUnits for the fluent API (Assert.That).
Current Refactored Code:
[TestMethod]
#if !NUNIT
[HostType("Moles")]
#else
[Moled]
#endif
public void LoadCSVBillOfMaterials_WithCorrectCSVFile_ReturnsListOfCSVBillOfMaterialsThatMatchesInput()
{
//arrange object(s)
var filePath = "Path Does Not Matter Because of Mole in File object";
string[] csvDataCorrectlyFormatted = { "1000, 1, Alt 1, , TBD, 1, 10.0, Notes, , Description, 2.50, ,A",
"1001, 1, Alt 2, , TBD, 1, 10.0, Notes, , Description, 2.50, ,A" };
var materialsExpected = new List<CSVMaterial>();
materialsExpected.Add(new CSVMaterial("1000", 1, "Alt 1", "TBD", 1m, 10.0m, "Notes", "Description", 2.50m,"A"));
materialsExpected.Add(new CSVMaterial("1001", 1, "Alt 2", "TBD", 1m, 10.0m, "Notes", "Description", 2.50m,"A"));
//by-pass actually hitting the file system and use in-memory representation of CSV file
MFile.ReadAllLinesString = s => csvDataCorrectlyFormatted;
//act on object(s)
var materialsActual = modCSVImport.LoadCSVBillOfMaterials(filePath);
//assert something happended
Assert.That(materialsActual.Count,Is.EqualTo(materialsExpected.Count));
materialsExpected.ForEach((anExpectedMaterial) => Assert.That(materialsActual.Contains(anExpectedMaterial)));
materialsActual.ForEach((anActualMaterial) => Assert.That(materialsExpected.Contains(anActualMaterial)));
}
Original Multi-Assert Unit-Test:
...
//1st element
Assert.AreEqual("1000", materials[0].PartNumber);
Assert.AreEqual(1, materials[0].SequentialItemNumber);
Assert.AreEqual("Alt 1", materials[0].AltPartNumber);
Assert.AreEqual("TBD", materials[0].VendorCode);
Assert.AreEqual(1m, materials[0].Quantity);
Assert.AreEqual(10.0m, materials[0].PartWeight);
Assert.AreEqual("Notes", materials[0].PartNotes);
Assert.AreEqual("Description", materials[0].PartDescription);
Assert.AreEqual(2.50m, materials[0].UnitCost);
Assert.AreEqual("A", materials[1].Revision);
//2nd element
Assert.AreEqual("1001", materials[1].PartNumber);
Assert.AreEqual(1, materials[1].SequentialItemNumber);
Assert.AreEqual("Alt 2", materials[1].AltPartNumber);
Assert.AreEqual("TBD", materials[1].VendorCode);
Assert.AreEqual(1m, materials[1].Quantity);
Assert.AreEqual(10.0m, materials[1].PartWeight);
Assert.AreEqual("Notes", materials[1].PartNotes);
Assert.AreEqual("Description", materials[1].PartDescription);
Assert.AreEqual(2.50m, materials[1].UnitCost);
Assert.AreEqual("A", materials[1].Revision);
}
如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。
绑定邮箱获取回复消息
由于您还没有绑定你的真实邮箱,如果其他用户或者作者回复了您的评论,将不能在第一时间通知您!
发布评论
评论(2)
我经常有不止一个断言。如果这都是测试一个逻辑工作单元的一部分,我认为这没有任何问题。
现在,我确实同意,如果您有一个覆盖
Equals
的类型,那么测试会比第二种形式简单得多。但在您的第一个测试中,看起来您实际上只想断言结果集合等于预期集合。我认为这在逻辑上是一个断言 - 只是当前您正在执行多个迷你断言来测试它。一些单元测试框架具有测试两个集合是否相等的方法 - 如果您使用的集合不相等,您可以轻松编写一个。我最近在我的“重新实现 LINQ to”中正是这样做的对象”博客系列,因为尽管 NUnit 提供了辅助方法,但其诊断并不是很有帮助。基本上,我对 MoreLINQ 的代码进行了轻微的重构。
I frequently have more than one assertion. If it's all part of testing one logical unit of work, I don't see any problem with that.
Now, I do agree that if you've got a type which overrides
Equals
, that makes tests much simpler than your second form. But in your first test, it looks you really just want to assert that the resulting collection equals an expected one. I think that's logically one assertion - it's just that currently you're performing multiple mini-assertions to test it.Some unit test frameworks have methods to test whether two collections are equal - and if the one you're using doesn't, you can easily write one. I recently did exactly this in my "reimplementing LINQ to Objects" blog series, because although NUnit provides a helper method, its diagnostics aren't terribly helpful. I refactored the code from MoreLINQ very slightly, basically.
这是我目前正在使用的重构。我重写了 CSVMaterial 的
ToString()
方法,并添加了更有用的断言消息。所以我认为这有助于提高代码的可读性和可维护性。它还使单元测试值得信赖(由于有用的诊断消息)。乔恩,感谢您对逻辑工作单元的思考。我重构的代码所做的事情与之前的迭代大致相同。两者仍然测试一件合乎逻辑的事情。另外,我还得研究一下 MoreLINQ 的东西。如果它在您的 C# InDepth 第二版书中,我会在从 Manning 购买 MEAP 版本时遇到它。感谢您的帮助。
This is the refactoring I'm currently using. I overrode the
ToString()
method of CSVMaterial and added a more useful assert message. So I think this helps with code readability and maintainability. It also makes the unit test trustworthy (due to the helpful diagnostic message).And Jon, thanks for the thought about a logical unit of work. My refactored code does about the same thing as the previous iteration. Both still test one logical thing. Also, I'll have to look into the MoreLINQ stuff. If it's in your C# InDepth 2nd edition book, I'll come across it as I bought the MEAP version from Manning. Thanks for your help.