如何测试在被测试的类中调用的方法?
我会开始说我对单元测试很新,我想开始使用TDD方法,但现在我正在为一些现有类编写单元测试来验证它们在所有情况下的功能。如何测试在被测试的类中调用的方法?
我已经能够使用NUnit和Rhino mocks测试我的大部分代码,没有太多麻烦。然而,我一直在想单元测试的功能,最终调用同一个类中的很多其他方法。我不能做类似
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,它调用几个被此相同的类中定义的其它方法。
那么重构此代码的建议策略是什么,使其更容易测试而不必存储子方法调用的值?有没有办法在被测试的类中伪造方法调用?
在此先感谢您的任何帮助或建议!
修改主题under test (SUT)。如果某件事很难进行单元测试,那么设计可能会很尴尬。
伪造方法调用中被测类导致过度指定的测试。结果是非常脆弱的测试:只要您修改或重构类,那么很可能您还需要修改单元测试。这导致单元测试的维护成本太高。
为了避免过度指定的测试,专注于公共方法。如果此方法在类中调用其他方法,请不要测试这些调用。另一方面:应该测试其他dependend on component (DOCs)的方法调用。
如果你坚持这一点,并有你在测试中错过一些重要的事情的感觉,那么它可能是一类或做太多的方法的标志。如果是上课:查找违规行为Single Responsibility Principle (SRP)。从中提取类并分别进行测试。在一种方法的情况下:用几种公开方法分解该方法并分别测试它们中的每一种。如果这仍然太尴尬,你肯定有一门课违反了SRP。
在特定情况下,你可以执行以下操作:提取方法ExportExcelDocToDataSet
和GetDataTableFromExcelSheet
成两个不同的类(也许叫他们ExcelToDataSetExporter
和ExcelSheetToDataTableExporter
)。包含这两种方法的原始类应该引用这两个类并调用您之前提取的那些方法。现在你可以单独测试所有三个类。应用Extract Class refactoring(book)来实现你的原始类的修改。
还要注意的是改装测试总是有点麻烦编写和维护。原因在于没有进行单元测试的SUT倾向于设计尴尬,因此很难进行测试。这意味着单元测试的问题必须通过修改SUT来解决,并且不能通过拉伸单元测试来解决。
我猜你是单独测试的公共方法GetDataTableFromExcelSheet
,因此对于ExportExcelDocToDataSet
测试你不需要验证的GetDataTableFromExcelSheet
行为(超出ExportExcelDocToDataSet
作品如预期的事实)。
一个常见的策略是只测试公共方法,因为任何支持公共方法的私有方法都会默认进行测试,如果公共方法按预期行为。
进一步考虑这一点,您只能测试一个类的行为,而不是将方法作为单元。这有助于防止您的测试变得脆弱 - 更改班级内部的地方往往会打破一些测试。
当然,您希望所有的代码都经过良好测试,但对方法过于专注会导致脆弱;测试课堂行为(它是否应该在最高级别进行测试)也会测试较低的级别。
如果您想从测试中伪造方法,您可以重构代码以为您想伪造的方法创建一个接口。请参阅command pattern。
在这种情况下,虽然明显的变化是ExportExcelDocToDataSet
将工作簿作为参数。在测试中,您可以发送一个假工作簿。请参阅inversion of control。
我有一个构造函数的IWorkbook对象嘲笑簿对象(我并没有显示在我的例子任何构造函数),所以这部分的照顾。 GetDataTableFromExcelSheet将是私人的,它不是我想要测试它。我明白你在说什么只是测试公共方法 - 由于更高层次上的一些方法的复杂性,我开放了很多以前的私有测试方法。我非常喜欢你的建议,因为我花了很多时间为每种方法编写测试,这可能不是正确的方法。 – 2012-03-11 20:35:59
引擎盖下测试的方法调用并不重要 - 这是实现细节和你的单元测试不应该很多意识到这一点。通常(当然,大多数的单元测试的时间)要测试单个单元并专注于这一点。
你既可以写在你的测试以外类别的功能类或重构部分的每个公共方法单独,孤立的测试。两种方法都集中在同一件事上 - 对每个单元进行单独测试。
现在,给你一些提示:
- 什么是你测试类的名称?基于它暴露的方法,沿着
ExcelExporterAndToDataSetConverter
...或ExcelManager
的行?看起来这个班可能在做too many things at once;这要求进行一些重构。将数据导出到DataSet可以轻松地将Excel数据转换为DataSets/DataRows。 - 时会发生什么
GetDataTableFromExcelSheet
方法的变化?获取移动到其他课程或由第三方代码?它应该打破你的出口测试吗?它不应该 - 这是你的出口测试不应该验证它是否被叫的原因之一。
我建议移动到数据集/ DataRow的转换方法,以单独的类 - 这将缓解单元测试和您的出口检验的写作不会是那样脆弱。
的类名是一个不错的猜测:这就是所谓的“ExcelHelper”这种想法是,它是由我的应用程序来处理有关事情的Excel的其他部分被称为奇异类。我应该至少创建一个ExcelImporter和ExcelExporter类,在阅读完这些评论后,我有一些想法可以进一步分解代码。我一直认为在将代码拆分成同一类中更小的私有方法来处理工作而不是编写更多具有特定用途的类时会进行重构。 – 2012-03-11 20:27:42
@GageTrader:*助手*,*经理*和所有隐约名为类通常表明SRP冲突。 'ExcelImporter'很好地传达了它的角色/目的。什么'ExcelHelper'沟通?帮助什么...?课堂命名困难是SRP违规的迹象之一 - 很难找到合适的课堂名称。你一定要重构你的一些代码 - 你的设计将会更清晰,更容易理解,并且更易于测试。 – 2012-03-11 20:38:39
有一件事是肯定的你正在做TDD正确的方式:) 在上面的代码中,你将不得不在测试ExportExcelDocToDataSet方法之前模拟GetDataTableFromExcelSheet方法。
但有一件事你可以做的是,而不是通过数据表从GetDataTableFromExcelSheet从你的代码,你加入另一个参数调用ExportExcelDocToDataSet方法的地方返回。
这样的事情
DataTable dtExcelData = GetData ....; 和修改方法如下
公共数据集ExportExcelDocToDataSet(布尔headerRowProvided,数据表dtExcelData)
这样你就不必测试ExportExcelDocToDataSet方法时嘲笑GetDataTableFromExcelSheet ExportExcelDocToDataSet方法里面。
关于传递DataTable而不是仅获取返回值的好处。我倾向于触发器如何最好地做到这一点:传入一个实例化的对象或将其返回给调用方法。尽管如此,我仍然有更多的依赖关系进入调用堆栈(未显示),所以我仍然会遇到原始问题,无法测试其中一些较低级别的方法。 – 2012-03-11 20:22:25
我曾经想过将你提到的方法提取到不同的类中,所以我可以使用AssertWasCalled扩展单元测试它们,但我不确定这是否是一个好主意,因为我可以看到导致大量很浅的类最多只有一种或两种方法。我一定会看看你推荐的书,还有更多关于SRP的书。关于这一切的最好的事情是我是这个项目中唯一的人,所有的代码都是由我编写的,所以我可以做任何我需要的工作来完成它。 – 2012-03-11 20:15:51
@量具交易者:不要用方法的数量来衡量一个类。例如,命令模式只显示一种方法,仍然是一种widley使用的技术。在应用单元测试和良好的面向对象设计原则之前,我对更高级别的类别感到不舒服。我花了一些时间意识到更多的班级并不意味着更多的工作。是的,确实需要更多的时间进行设计和测试,但更高的质量和更低的错误数量弥补了这一点。 – 2012-03-12 10:51:54