我正试图为一个巨大的项目编写单元测试,其中在编码时从未考虑过可测试性。我已经开始模拟对象并编写测试,但我意识到为了能够模拟它,我必须重构大量代码。
这是我想要创建测试的方法之一:
public List<DctmViewDefinition> GetDctmViewDefinitions()
{
List<DctmViewDefinition> dctmViewDefinitions = new List<DctmViewDefinition>();
DataPackage dataPackage = MyDfsUtil.GetObjectsWithContent();
foreach (DataObject dataObject in dataPackage.DataObjects)
{
DctmViewDefinition view = GetDctmViewDefinitionFromXmlFile(dataObject);
dctmViewDefinitions.Add(view);
}
return dctmViewDefinitions;
}
mydfsutil类处理web服务调用,我想模拟它。MyDfsUtil分为14个部分类,每个部分类由300-500行代码组成。所以有很多代码!
这是一个类的摘录给你的想法:
public partial class MyDfsUtil
{
public string Locale { get; set; }
public string DfsServiceUrl { get; set; }
public string UserName { get; set; }
public DataPackage GetObjectsWithContent()
{
//Some code here
}
}
我正在使用Moq,因此我不能直接模拟这个类(据我所知)。我必须要么创建一个接口,一个抽象类,要么使方法成为虚拟的。所以,我一直在试图找出的是:为了能够嘲笑MyDfsUtil,最好的方法是什么?
首先,我想创建一个接口,但是变量(Locale, UserName等)使用的所有代码?
其次,我试图用所有变量创建一个抽象基类MyDfsUtilBase,并使基类中的方法返回NotImplementedException。这样的:
public abstract class MyDfsUtilBase
{
public string Locale { get; set; }
public string DfsServiceUrl { get; set; }
public string UserName { get; set; }
public void GetObjectsWithContent()
{
throw new NotImplementedException();
}
}
然后Resharper告诉我在mydfsutil类的GetObjectsWithContent()-实现中添加'new'关键字。或者我可以在基类中声明我的方法为virtual,然后在实现上使用'override'关键字。但是如果我必须声明我的方法是虚拟的,我可以在MyDfsUtil中这样做,然后我不需要创建一个抽象基类。我一直在阅读关于虚拟方法的文章,似乎人们在是否使用它们的问题上意见不一。在MyDfsUtil中使用虚拟方法将使我的重构分配更容易,并且使我能够模拟它们。对于像我这样的案例,有没有什么最佳实践?
我正试着用最好、最简单的方法来做这件事。我没有单元测试或模拟的经验,我真的希望在不引入太多复杂性的情况下这样做。
首先,我想创建一个界面,但是变量(区域设置,用户名等)使用的所有代码?
可以在接口中包含属性。
对于像我这样的情况有什么最佳实践吗?
我建议你使用接口隔离原则并创建一堆小接口,这些接口将由你的MyDfsUtil
类实现:
public interface IDfsService
{
string Locale { get; set; }
string DfsServiceUrl { get; set; }
string UserName { get; set; }
}
public interface IDataPackageService : IDfsService
{
DataPackage GetObjectsWithContent()
}
public interface IFooService : IDfsService
{
Foo GetFoo();
void DoSomethingWithFoo();
}
使MyDfsUtil
实现这些小接口
public partial class MyDfsUtil : IDataPackageService, IFooService
{
public string Locale { get; set; }
public string DfsServiceUrl { get; set; }
public string UserName { get; set; }
public DataPackage GetObjectsWithContent()
{
//Some code here
}
// ...
}
然后让其他类依赖于小接口,而不是使用这个庞大的类。例如,你的类只能依赖于IDataPackageService
。
好处:
- 你现在不需要重构你的怪物类。从客户的角度来看,它看起来像是已经重构过了。以后你可以用基类拆分成小的类,并做其他的重构。你不需要处理怪物类的所有成员。如果您正在测试只使用方法A、B和C的客户机,那么通过引入小而简单的接口来反转客户机和
- 这就像由外而内的开发——在为客户端编写测试之后,你将有一组客户端需要的接口(顺便说一句,你会感到惊讶——可能会发生一些甚至许多
MyDfsUtil
方法没有被任何客户端使用的情况)。MyDfsUtil
的进一步重构将会容易得多,因为你不需要考虑如何将它的功能分组到更小的类中——这些类已经被接口定义了。
MyDfsUtil
之间的依赖关系。易于模仿,易于理解。三年前我就和你一样。
我给你的建议是不要碰MyDfsUtil
,不要碰它。
(我猜这是一个静态类的静态方法?)
创建一个接口和匹配的类(比如ISaneMyDfsUtil
&SaneMyDfsUtil
)
从作为示例的GetDctmViewDefinitions
方法开始,将MyDfsUtil
方法添加到新类和接口中,它使用GetObjectsWithContent
。这个新类上的"new"方法只是直接委托给现有的、不可测试的MyDfsUtil
类。你将这个类的一个实例注入到被测试的类中。
这样做有多种原因。
使MyDfsUtil
可模拟可能并不理想。
- 类可能在整个项目的各个级别的代码中使用。测试一个方法很快就需要你模拟——详细地——它的几个方法。
- 类是方式大,需要被重构成不同的类与单一的职责。您可以通过滚动
MyDfsUtil
上的不同接口和类来实现这一点。当你有时间的时候,功能可以从MyDfsUtil
中出来,放到它真正所属的新类中。 -
MyDfsUtil
中的方法可能会为您的用例返回太多。例如,假设您正在测试的方法需要MyDfsUtil
中的客户id列表。调用MyDfsUtil.QueryCustomers(myOrderId);
,它返回客户列表。您的代码执行操作,并且只使用客户的Id属性。在模拟该调用时,必须创建客户对象、设置id并传回客户列表。在SaneMyDfsUtil
中,您可以有一个QueryCustomerIds
方法,该方法使仅返回客户id。它使被测代码更显式,并使测试的模拟更简单。
我这里有一些遗留软件,使用带有数百(如果不是数千)个方法的静态Dal对象。我编写了一些代码,为它自动生成Sane_Object类和接口。随着引入接缝进行测试的努力,它并不是很糟糕,但我及时了解到它远非理想的,遵循我在这里列出的模式将节省时间和精力,并将帮助我以更容易的方式将单元测试推向团队。
我现在可以回答我自己的问题,说,不,这不是一个好主意。
最后的一句话,在你做太多其他事情之前,先阅读《单元测试的艺术》(诚实地购买并从头到尾阅读)。然后继续有效地使用桌上的遗留代码,深入研究它,并将其作为遇到困难时的参考。
有问题就喊