【问题标题】:How do I test a method was called within a class under test?如何测试在被测类中调用的方法?
【发布时间】:2012-03-28 07:23:58
【问题描述】:

我首先要说我对单元测试很陌生,我想开始使用 TDD 方法,但现在我正在为一些现有类编写单元测试以验证它们在所有情况下的功能。

我已经能够使用 NUnit 和 Rhino 模拟测试我的大部分代码而没有太多麻烦。但是,我一直想知道单元测试函数最终会在同一个类中调用许多其他方法。我不能做类似的事情

classUnderTest.AssertWasCalled(cut => cut.SomeMethod(someArgs))

因为被测试的类不是假的。此外,如果我正在测试的方法调用了被测类中的其他方法,而这些方法又调用了同一类中的方法,那么我将需要伪造大量值来测试“顶级”方法。由于我也在对所有这些“子方法”进行单元测试,因此如果“SomeMethod”通过了单元测试,我应该能够假设它按预期工作,并且不需要担心那些低级方法的细节。

这是我一直在使用的一些示例代码来帮助说明我的观点(我编写了一个类来使用 NPOI 管理 Excel 文件的导入/导出):

    public DataSet ExportExcelDocToDataSet(bool headerRowProvided)
    {
        DataSet ds = new DataSet();

        for (int i = 0; i < currentWorkbook.NumberOfSheets; i++)
        {               
            ISheet tmpSheet = currentWorkbook.GetSheetAt(i);

            if (tmpSheet.PhysicalNumberOfRows == 0) { continue; }
            DataTable dt = GetDataTableFromExcelSheet(headerRowProvided, ds, tmpSheet);

            if (dt.Rows.Count > 0)
            {
                AddNonEmptyTableToDataSet(ds, dt);
            }
        }

        return ds;
    }

    public DataTable GetDataTableFromExcelSheet(bool headerRowProvided, DataSet ds, ISheet tmpSheet)
    {
        DataTable dt = new DataTable();
        for (int sheetRowIndex = 0; sheetRowIndex <= tmpSheet.LastRowNum; sheetRowIndex++)
        {
            DataRow dataRow = GetDataRowFromExcelRow(dt, tmpSheet, headerRowProvided, sheetRowIndex);
            if (dataRow != null && dataRow.ItemArray.Count<object>(obj => obj != DBNull.Value) > 0)
            {
                dt.Rows.Add(dataRow);
            }
        }

        return dt;
    }

...

您可以看到 ExportExcelDocToDataSet(在本例中是我的“顶级”方法)调用 GetDataTableFromExcelSheet,后者调用 GetDataRowFromExcelRow,后者调用在同一个类中定义的其他几个方法。

那么,重构此代码以使其更易于单元测试而不必存根由子方法调用的值的推荐策略是什么?有没有办法在被测类中伪造方法调用?

提前感谢您的任何帮助或建议!

【问题讨论】:

    标签: c# unit-testing refactoring nunit rhino-mocks


    【解决方案1】:

    修改主题under test (SUT)。如果某些东西难以进行单元测试,那么设计可能会很尴尬。

    在被测类中伪造方法调用会导致超出指定的测试。结果是非常脆弱的测试:一旦您修改或重构了类,那么您很可能还需要修改单元测试。这会导致单元测试的维护成本过高。

    为避免过度指定的测试,请专注于公共方法。如果此方法调用类中的其他方法,请不要测试这些调用。另一方面:应该测试对其他dependend on component (DOCs) 的方法调用。

    如果你坚持这一点并且感觉你在测试中遗漏了一些重要的东西,那么这可能是一个类或方法做得太多的迹象。如果是班级:查找违反Single Responsibility Principle (SRP) 的情况。从中提取类并单独测试它们。如果是方法:将方法拆分为几个公共方法并分别测试它们。如果这仍然太尴尬,那么你肯定有一个违反 SRP 的类。

    在您的具体情况下,您可以执行以下操作:将方法 ExportExcelDocToDataSetGetDataTableFromExcelSheet 提取到两个不同的类中(可能称它们为 ExcelToDataSetExporterExcelSheetToDataTableExporter)。包含这两个方法的原始类应该引用这两个类并调用您之前提取的那些方法。现在您可以单独测试所有三个类。应用Extract Class refactoring (book) 来实现对原有类的修改。

    还要注意,改造测试的编写和维护总是有点麻烦。原因是没有单元测试的 SUT 往往有一个笨拙的设计,因此更难测试。这意味着单元测试的问题必须通过修改SUT来解决,而不能通过单元测试来解决。

    【讨论】:

    • 我曾想过将您提到的方法提取到单独的类中,这样我就可以使用 AssertWasCalled 扩展对它们进行单元测试,但我不确定这是否是个好主意,因为我可以看到这会导致大量非常浅的类,其中最多只有一两个方法。我一定会查看您推荐的书以及有关 SRP 的更多信息。最好的一点是我是项目中唯一的人,所有的代码都是我写的,所以我可以做任何我需要做的事情。
    • @Gage Trader:不要根据方法的数量来衡量一个类。例如,命令模式仅公开一种方法,并且仍然是一种广泛使用的技术。在我应用单元测试和良好的 OO 设计原则之前,我对更多的类感到不舒服。我花了一些时间才意识到,更多的课程并不意味着更多的工作。是的,您确实需要更多时间进行设计和测试,但到目前为止,更高的质量和更少的错误数量弥补了这一点。
    【解决方案2】:

    在底层调用什么测试方法并不重要——这是实现细节,你的单元测试不应该太多意识到这一点。通常(嗯,大多数时候是单元测试)你想测试单个单元并专注于那个。

    您可以为类中的每个公共方法编写单独的、隔离的测试,也可以在外部重构测试类的部分功能。两种方法都专注于同一件事 - 为每个 unit 单独测试。

    现在,给你一些提示:

    • 您测试的班级的名称是什么?基于它公开的方法,类似于ExcelExporterAndToDataSetConverter ...或ExcelManager?看来这门课可能在做too many things at once;这要求进行一些重构。将数据导出到 DataSet 可以很容易地与将 excel 数据转换为 DataSets/DataRows 分开。
    • GetDataTableFromExcelSheet 方法改变时会发生什么?被转移到其他班级或被第 3 方代码取代?它应该打破你的出口测试吗?它不应该 - 这是您的导出测试不应该验证它是否被调用的原因之一。

    我建议使用 DataSet/DataRow 转换方法来分离类 - 这将简化单元测试的编写,并且您的导出测试不会那么脆弱。

    【讨论】:

    • 对类名的好猜测:它被称为“ExcelHelper”,其想法是让我的应用程序的其余部分调用它来处理与 Excel 相关的任何事情。我可能应该至少创建一个 ExcelImporter 和 ExcelExporter 类,在阅读了这些 cmets 之后,我对进一步拆分代码有了一些想法。我一直在考虑重构,将代码拆分为同一个类中更小的私有方法来处理工作,而不是编写更多具有特定目的的类。
    • @GageTrader:helpersmanagers 和所有名称模糊的类通常表明违反了 SRP。 ExcelImporter 很好地传达了它的角色/目的。 ExcelHelper 交流什么?帮助什么...?类命名困难是违反 SRP 的标志之一——很难为类找到合适的名称。你绝对应该重构你的一些代码——你的设计会更简洁、更容易理解、更容易测试。
    【解决方案3】:

    我猜你正在单独测试公共方法GetDataTableFromExcelSheet,所以对于ExportExcelDocToDataSet 的测试,你不需要验证GetDataTableFromExcelSheet 的行为(除了ExportExcelDocToDataSet 按预期工作的事实)。

    一种常见的策略是仅测试公共方法,因为如果公共方法的行为符合预期,则默认测试任何支持您的公共方法的私有方法。

    更进一步,您可以只测试一个类的行为,而不是以方法为单位。这有助于防止你的测试变得脆弱——改变类的内部结构往往会破坏你的一些测试。

    当然,您希望所有代码都经过良好测试,但是过于关注方法会导致脆弱;测试类行为(它是否在最高级别做它应该做的事情)也测试较低级别。

    如果你想从测试中伪造方法,你可以重构你的代码来为你想要伪造的方法获取一个接口。请参阅command pattern

    在这种情况下,虽然明显的变化是ExportExcelDocToDataSet 将工作簿作为参数。然后,在测试中,您可以发送伪造的工作簿。见inversion of control

    【讨论】:

    • 我确实有一个构造函数,它接受一个 IWorkbook 对象来模拟工作簿对象(在我的示例中我没有显示任何构造函数),因此该部分得到了处理。如果我不想测试它,GetDataTableFromExcelSheet 将是私有的。我明白您所说的仅测试公共方法的内容-由于更高级别的某些方法的复杂性,我一直在开放许多以前的私有方法进行测试。我非常喜欢你的建议,因为我花了很多时间为我拥有的每种方法编写测试,这可能不是正确的方法。
    【解决方案4】:

    有一件事是肯定的,你正在以正确的方式进行 TDD :) 在上面的代码中,您必须在测试 ExportExcelDocToDataSet 方法之前模拟 GetDataTableFromExcelSheet 方法。

    但您可以做的一件事是通过添加另一个参数,从代码中调用 ExportExcelDocToDataSet 方法的位置传递从 GetDataTableFromExcelSheet 返回的数据表。

    类似的东西

    DataTable dtExcelData = GetData....; 并修改方法如下

    public DataSet ExportExcelDocToDataSet(bool headerRowProvided, DataTable dtExcelData)

    这样您在测试 ExportExcelDocToDataSet 方法时就不需要在 ExportExcelDocToDataSet 方法中模拟 GetDataTableFromExcelSheet。

    【讨论】:

    • 关于传递 DataTable 而不是仅仅获取返回值的好点。我倾向于考虑如何最好地做到这一点:传入一个实例化的对象或将其返回给调用方法。不过,在调用堆栈的下方(未显示)我仍然有更多依赖项,所以我仍然会遇到无法测试其中一些较低级别方法的原始问题。
    猜你喜欢
    • 1970-01-01
    • 2013-06-04
    • 1970-01-01
    • 2020-05-01
    • 2023-03-05
    • 2014-05-29
    • 2011-07-16
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多