重构和模拟以支持单元测试

本文关键字:支持 单元测试 模拟 重构 | 更新日期: 2023-09-27 17:49:37

我正试图为一个巨大的项目编写单元测试,其中在编码时从未考虑过可测试性。我已经开始模拟对象并编写测试,但我意识到为了能够模拟它,我必须重构大量代码。

这是我想要创建测试的方法之一:

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可模拟可能并不理想。

  1. 类可能在整个项目的各个级别的代码中使用。测试一个方法很快就需要你模拟——详细地——它的几个方法。
  2. 类是方式大,需要被重构成不同的类与单一的职责。您可以通过滚动MyDfsUtil上的不同接口和类来实现这一点。当你有时间的时候,功能可以从MyDfsUtil中出来,放到它真正所属的新类中。
  3. MyDfsUtil中的方法可能会为您的用例返回太多。例如,假设您正在测试的方法需要MyDfsUtil中的客户id列表。调用MyDfsUtil.QueryCustomers(myOrderId);,它返回客户列表。您的代码执行操作,并且只使用客户的Id属性。在模拟该调用时,必须创建客户对象、设置id并传回客户列表。在SaneMyDfsUtil中,您可以有一个QueryCustomerIds方法,该方法使返回客户id。它使被测代码更显式,并使测试的模拟更简单。

我这里有一些遗留软件,使用带有数百(如果不是数千)个方法的静态Dal对象。我编写了一些代码,为它自动生成Sane_Object类和接口。随着引入接缝进行测试的努力,它并不是很糟糕,但我及时了解到它远非理想的,遵循我在这里列出的模式将节省时间和精力,并将帮助我以更容易的方式将单元测试推向团队。

我现在可以回答我自己的问题,说,不,这不是一个好主意。

最后的一句话,在你做太多其他事情之前,先阅读《单元测试的艺术》(诚实地购买并从头到尾阅读)。然后继续有效地使用桌上的遗留代码,深入研究它,并将其作为遇到困难时的参考。

有问题就喊