我首先要说的是我对单元测试很新,我想开始使用TDD方法,但是现在我正在编写一些现有类的单元测试来验证它们在所有情况下的功能。
我已经能够使用NUnit和Rhino模拟测试我的大多数代码而没有太多麻烦。但是,我一直想知道单元测试函数最终会在同一个类中调用很多其他方法。我不能做像
这样的事情classUnderTest.AssertWasCalled(cut => cut.SomeMethod(someArgs))
因为被测试的课程不是假的。此外,如果我正在测试的方法调用被测试类中的其他方法,而这些方法又调用同一类中的方法,那么我将需要伪造大量值来测试“顶级”方法。由于我也是对所有这些“子方法”进行单元测试,我应该能够假设“SomeMethod”如果通过单元测试而无需担心这些低级方法的详细信息就能按预期工作。 / p>
以下是我一直在使用的示例代码,以帮助说明我的观点(我编写了一个类来管理使用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 ,调用在同一个类中定义的其他几种方法。
那么重构这段代码的建议策略是什么,使其更加单元可测试而不必使用子方法调用的存根值?有没有办法在被测试的类中伪造方法调用?
提前感谢您的任何帮助或建议!
答案 0 :(得分:6)
修改主题under test (SUT)。如果某些东西难以进行单元测试,那么设计可能会很尴尬。
在测试类中的伪造方法调用会导致超过指定的测试。结果是非常脆弱的测试:只要你修改或重构类,那么你很可能还需要修改单元测试。这导致单元测试的维护成本过高。
为避免过度指定的测试,请专注于公共方法。如果此方法调用类中的其他方法,请不要测试这些调用。另一方面:应该测试其他dependend on component (DOCs)上的方法调用。
如果你坚持这一点,并且觉得你在测试中错过了一些重要的东西,那么这可能是一个做得太多的课程或方法的标志。如果是班级:查找违反Single Responsibility Principle (SRP)的内容。从中提取类并单独测试它们。在方法的情况下:在几个公共方法中将方法拆分并分别测试每个方法。如果这仍然太尴尬,你肯定有一个违反SRP的课程。
在您的具体情况下,您可以执行以下操作:将方法ExportExcelDocToDataSet
和GetDataTableFromExcelSheet
提取到两个不同的类中(可以称之为ExcelToDataSetExporter
和ExcelSheetToDataTableExporter
)。包含这两种方法的原始类应该引用这两个类并调用之前提取的那些方法。现在,您可以单独测试所有三个类。应用Extract Class refactoring(book)来修改原始类。
另请注意,改装测试总是有点麻烦,无法编写和维护。原因是没有进行单元测试而编写的SUT往往设计笨拙,因此难以测试。这意味着单元测试的问题必须通过修改SUT来解决,并且无法通过拉伸单元测试来解决。
答案 1 :(得分:2)
测试方法调用的内容并不重要 - 这是实现细节,您的单元测试不应该很多意识到这一点。通常(好吧,大部分时间用于单元测试)你想测试单个单元并专注于它。
您可以为类中的每个公共方法编写单独的隔离测试,也可以重构外部测试类的部分功能。两种方法都集中在同一个方面 - 对每个单元进行隔离测试。
现在,给你一些提示:
ExcelExporterAndToDataSetConverter
...或ExcelManager
一致?看来这堂课可能正在做too many things at once;这要求进行一些重构。将数据导出到DataSet可以很容易地将excel数据转换为DataSets / DataRows。GetDataTableFromExcelSheet
方法发生变化时会发生什么?被移动到其他类或被第三方代码取代?它应该打破你的出口测试吗?它不应该 - 这是导出测试不应该验证它是否被调用的原因之一。我建议将DataSet / DataRow转换方法转移到单独的类 - 它将简化单元测试的编写,并且您的导出测试不会那么脆弱。
答案 2 :(得分:1)
我猜你是在分别测试公共方法GetDataTableFromExcelSheet
,所以对于ExportExcelDocToDataSet
的测试,你不需要验证GetDataTableFromExcelSheet
的行为(除ExportExcelDocToDataSet
之外的事实1}}按预期工作。)
一种常见的策略是仅测试公共方法,因为如果公共方法按预期运行,则默认情况下会测试任何支持公共方法的私有方法。
进一步说,你可以只测试一个类的行为,而不是把方法作为单位。这有助于防止你的测试变得脆弱 - 改变课程内部的倾向会破坏你的一些测试。
当然,您希望所有代码都经过充分测试,但过于专注于方法会导致脆弱;测试类行为(它是否在最高级别执行它应该执行的操作)也测试较低级别。
如果你想从测试中伪造方法,你可以重构你的代码,为你想要伪造的方法取一个接口。请参阅command pattern。
在这种情况下,虽然明显的变化是ExportExcelDocToDataSet
将工作簿作为参数。在测试中,您可以发送假工作簿。请参阅inversion of control。
答案 3 :(得分:0)
有一件事是肯定你正在以正确的方式做TDD :) 那么在上面的代码中,你必须在测试ExportExcelDocToDataSet方法之前模拟GetDataTableFromExcelSheet方法。
但是,您可以做的一件事是通过添加另一个参数,从您调用ExportExcelDocToDataSet方法的代码中的位置传递从GetDataTableFromExcelSheet返回的数据表。
类似这样的事情
DataTable dtExcelData = GetData ....; 并修改如下方法
public DataSet ExportExcelDocToDataSet(bool headerRowProvided,DataTable dtExcelData)
这样,在测试ExportExcelDocToDataSet方法时,您无需在ExportExcelDocToDataSet方法内部模拟GetDataTableFromExcelSheet。