I'll start by saying I am pretty new to unit testing and I'd like to start using a TDD approach, but for now am writing unit tests for some existing classes to verify their functionality in all cases.
I've been able to test the majority of my code using NUnit and Rhino mocks without much trouble. However, I've been wondering about unit testing functions that end up calling a lot of other methods within the same class. I can't do something like
classUnderTest.AssertWasCalled(cut => cut.SomeMethod(someArgs))
because the class under test isn't a fake. Furthermore, if a method I'm testing calls other methods in the class under test that in turn also call methods in the same class, I'm going to need to fake a ton of values just to test the "top level" method. Since I'm also unit testing all of these "sub methods", I should be able to assume that "SomeMethod" works as expected if it passes the unit test and not need to worry about the details of those lower-level methods.
Here is some example code I've been working with to help illustrate my point (I've written a class to manage import/export of Excel files using NPOI):
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;
}
...
You can see that ExportExcelDocToDataSet (my "top-level" method in this case) calls GetDataTableFromExcelSheet which calls GetDataRowFromExcelRow, which calls a couple of other methods that are defined within this same class.
So what is the recommended strategy for refactoring this code to make it more unit testable without having to stub values called by submethods? Is there a way to fake method calls within the class under test?
Thanks in advance for any help or advice!
Modify the subject under test (SUT). If something is hard to unit test, then the design might be awkward.
Faking method calls within the class under test leads to over specified tests. The result are very brittle tests: As soon as you modify or refactor the class, then it is very likely that you also need modify the unit tests. This leads too high maintenance costs of unit testing.
To avoid over specified tests, concentrate on public methods. If this method calls other methods within the class, do not test these calls. On the other hand: Method calls on other dependend on component (DOCs) should be tested.
If you stick to that and have the feeling that you miss some important thing in your tests, then it might be a sign for a class or a method which is doing too much. In case of a class: Look for violations of the Single Responsibility Principle (SRP). Extract classes out of it and test them separately. In case of a method: Split it up the method in several public methods and test each of them separately. If this is still too awkward, you definitely have a class which violates the SRP.
In your specific case you can do the following: Extract the methods ExportExcelDocToDataSet
and GetDataTableFromExcelSheet
into two different classes (maybe call them ExcelToDataSetExporter
and ExcelSheetToDataTableExporter
). The original class which contained both methods should reference both classes and call those methods, which you previously extracted. Now you are able to test all three classes in isolation. Apply the Extract Class refactoring (book) to achieve the modification of your original class.
Also note that retrofitting tests are always a bit cumbersome to write and maintain. The reason is that the SUTs, which are written without unit tests, tend to have an awkward design and thus are harder to test. This means that the problems with unit tests must be solved by modifying the SUTs and cannot be solved by pimping up the unit tests.