在处理List <t>对象时,重构单元测试的挑战是可维护和可读的</t>

时间:2010-09-26 06:22:38

标签: unit-testing list refactoring code-readability code-maintainability

在书The Art of Unit Testing中,它谈到了想要创建可维护和可读的单元测试。在第204页左右,它提到应该尝试在一次测试中避免多个断言,并且可能使用重写的Equals方法比较对象。当我们只有一个对象来比较预期结果和实际结果时,这很有效。但是,如果我们有所述对象的列表(或集合),该怎么办呢?

考虑下面的测试。我有多个断言。实际上,有两个独立的循环调用断言。在这种情况下,我将最终得到5个断言。 2检查另一个列表中存在的内容,反之亦然。第五个比较列表中的元素数量。

如果有人有改进此测试的建议,我会全力以赴。我现在正在使用MSTest,尽管我用NUnits替换了MSTest的Assert用于流畅的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);
        }

2 个答案:

答案 0 :(得分:1)

我经常有多个断言。如果它是测试一个逻辑工作单元的全部内容,我认为没有任何问题。

现在,我同意如果你有一个覆盖Equals的类型,这使得测试比第二种形式简单得多。但是在你的第一次测试中,它看起来你真的只想断言结果集合等于预期的集合。我认为这在逻辑上是一个断言 - 只是目前你正在执行多个迷你断言来测试它。

某些单元测试框架具有测试两个集合是否相等的方法 - 如果您使用的集合不相同,则可以轻松编写一个。我最近在我的"reimplementing LINQ to Objects" blog series中做了这个,因为尽管NUnit提供了一个辅助方法,但它的诊断并不是非常有用。我基本上稍微重构了MoreLINQ的代码。

答案 1 :(得分:0)

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

Jon,感谢您对逻辑工作单元的思考。我重构的代码与前一次迭代的内容大致相同。两者仍然测试一个逻辑的东西。另外,我将不得不研究MoreLINQ的内容。如果它出现在你的C#InDepth第2版书中,那么当我从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());
}