在处理 List时重构单元测试以使其可维护和可读的挑战物体

发布于 2024-09-25 02:52:21 字数 3473 浏览 2 评论 0原文

单元测试的艺术一书中,它谈到了想要创建可维护和可读的单元测试。在第 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 技术交流群。

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

发布评论

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

评论(2

假装不在乎 2024-10-02 02:52:21

我经常有不止一个断言。如果这都是测试一个逻辑工作单元的一部分,我认为这没有任何问题。

现在,我确实同意,如果您有一个覆盖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.

小巷里的女流氓 2024-10-02 02:52:21

这是我目前正在使用的重构。我重写了 CSVMaterial 的 ToString() 方法,并添加了更有用的断言消息。所以我认为这有助于提高代码的可读性和可维护性。它还使单元测试值得信赖(由于有用的诊断消息)。

乔恩,感谢您对逻辑工作单元的思考。我重构的代码所做的事情与之前的迭代大致相同。两者仍然测试一件合乎逻辑的事情。另外,我还得研究一下 MoreLINQ 的东西。如果它在您的 C# InDepth 第二版书中,我会在从 Manning 购买 MEAP 版本时遇到它。感谢您的帮助。

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("1001", 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            

    //Setup message for failed asserts
    var assertMessage = new StringBuilder();
    assertMessage.AppendLine("Actual Materials:");
    materialsActual.ForEach((m) => assertMessage.AppendLine(m.ToString()));
    assertMessage.AppendLine("Expected Materials:");
    materialsExpected.ForEach((m) => assertMessage.AppendLine(m.ToString()));

    Assert.That(materialsActual, Is.EquivalentTo(materialsExpected),assertMessage.ToString());
}

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.

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("1001", 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            

    //Setup message for failed asserts
    var assertMessage = new StringBuilder();
    assertMessage.AppendLine("Actual Materials:");
    materialsActual.ForEach((m) => assertMessage.AppendLine(m.ToString()));
    assertMessage.AppendLine("Expected Materials:");
    materialsExpected.ForEach((m) => assertMessage.AppendLine(m.ToString()));

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