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

时间:2014-03-12 11:45:58

标签: c# unit-testing refactoring moq

我正在尝试为一个巨大的项目编写单元测试,在这个项目中,编码时从未尝试过可测试性。我开始嘲笑对象并编写测试,但我意识到我必须重构很多代码才能模拟它。

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

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-class处理webservice-calls,我想模仿它。 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告诉我要添加新的&#39;我的GetObjectsWithContent()的关键字 - MyDfsUtil-class中的实现。或者我可以将基类中的方法声明为虚拟,然后在实现上使用&#39;覆盖&#39; -keyword。但是如果我必须将我的方法声明为虚拟,我可以在MyDfsUtil中执行此操作,然后我不需要创建抽象基类。 我一直在阅读虚拟方法,似乎人们不同意是否使用它们。在MyDfsUtil中使用虚拟方法将使我的重构分配更容易,并且它使我能够模拟它们。对我这样的案件有什么最好的做法吗?

我试图以最好,最简单的方式做到这一点。我没有经验单元测试或嘲笑,我真的想在不引入太多复杂性的情况下这样做。

2 个答案:

答案 0 :(得分:3)

  

首先,我正在考虑创建一个界面,但是呢   整个代码使用的变量(Locale,UserName等)?

您可以在界面中包含属性。

  

对于像我这样的案件有没有最好的做法?

我建议您使用Interface Segregation Principle并创建一系列由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的进一步重构将更容易,因为您不会考虑如何将其功能分组为较小的类 - 这些类已经由接口定义。

答案 1 :(得分:1)

我正好在三年或更久以前。

我对你的建议是单独离开MyDfsUtil,不要碰它 (我认为它是静态方法的静态类?)

而是创建一个界面和匹配的类(比如说ISaneMyDfsUtil&amp; SaneMyDfsUtil

从您提供的一个方法开始GetDctmViewDefinitions添加到新类并使用MyDfsUtil方法接口GetObjectsWithContent。这个&#34;新&#34;新类的方法只是直接委托给现有的 - 和不可测试的MyDfsUtil类。您将此类的实例注入到测试类中。

这样做有多种原因。

使MyDfsUtil可模拟可能并不理想。

  1. 该类可能用于整个项目的各种代码级别。测试一种方法很快就会要求你详细模拟一些方法。
  2. 该课程通向大学,需要重新考虑到具有单一职责的不同班级。您可以通过滚动MyDfsUtil上的不同接口和类来实现。及时 - 当你有时间时 - 功能可以来自MyDfsUtil并进入它实际所属的新类。
  3. MyDfsUtil中的方法可能对您的用例返回太多。例如说您正在测试的方法需要MyDfsUtil的客户ID列表。您调用MyDfsUtil.QueryCustomers(myOrderId);,返回客户列表。您执行并且仅使用客户的Id属性的代码。模拟该调用时,您必须创建客户对象,设置ID,并传回客户列表。在SaneMyDfsUtil中,您可以使用QueryCustomerIds方法返回客户ID。它使得测试中的代码更加明确,并使测试更简单。
  4. 我在这里有一些遗留软件,它使用了一个静态Dal对象,有数百种(如果不是数千种)方法。我写了一些自动为它生成 Sane_Object 类和接口的代码。由于努力引入接缝进行测试,它并不是很强大,但我及时了解到它远非理想,并遵循我在这里布置的模式可以节省时间和努力,并帮助我以更轻松的方式将单元测试推向团队。

    我现在可以回答my own question并说,不,这不是一个好主意。

    在你做了太多其他事情之前,最后一句话是The Art of Unit Testing(诚实地购买并从头到尾阅读) 然后将Working Effectively with Legacy Code放在你的桌子上,浸入其中并将其作为参考,以便当事情变得艰难时。

    任何问题只是大喊

相关问题