【问题标题】:Refactoring and mocking to support unit testing重构和模拟以支持单元测试
【发布时间】:2014-03-12 11:45:58
【问题描述】:

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

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

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
    }

}

我使用的是起订量,因此我不能直接模拟这个类(据我所知)。我必须要么创建一个接口、一个抽象类,要么让这些方法成为虚拟的。 所以,我一直试图找出的是:为了能够模拟 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”关键字。或者我可以将基类中的方法声明为虚拟方法,然后在实现上使用“覆盖”关键字。但是如果我必须声明我的方法是虚拟的,我可以在 MyDfsUtil 中做到这一点,然后我不需要创建一个抽象基类。 我一直在阅读有关虚拟方法的信息,似乎人们在是否使用它们上意见不一。在 MyDfsUtil 中使用虚拟方法将使我的重构分配更容易,并且使我能够模拟它们。像我这样的案例有什么最佳实践吗?

我正在尝试以最好、最简单的方式做到这一点。我没有单元测试或模拟的经验,我真的很想在不引入太多复杂性的情况下做到这一点。

【问题讨论】:

    标签: c# unit-testing refactoring moq


    【解决方案1】:

    首先,我想创建一个界面,但是 变量(语言环境、用户名等)在整个代码中使用?

    您可以在接口中包含属性。

    像我这样的案例有什么最佳实践吗?

    我建议您使用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 的进一步重构会容易得多,因为您不会考虑如何将其功能分组到更小的类中——这些类已经由接口定义。

    【讨论】:

    • 所以我猜你不喜欢声明我想测试虚拟的所有方法的方法?
    • @amgravem 完全是 - 最好依赖抽象(接口是完美的抽象类型)。它很容易被嘲笑。它不需要对 MyDfsUtil 进行任何修改(嗯,你只需要列出它实现的接口)
    • 是的,这绝对会让我们的代码更易于管理。多年来,它已经成长为一个怪物。修复它感觉很好!
    • @amgravem 谢谢,顺便说一句,我已经添加了这种方法的另一个重要好处
    • @BinaryWorrier 是的,我也有类似的问题 :) 啊,遗留代码......谁写的?
    【解决方案2】:

    三年或更长时间前,我就在你所在的地方。

    我对你的建议是不要碰MyDfsUtil,不要碰它。
    (我假设它是一个带有静态方法的静态类?)

    而是创建一个接口和匹配类(比如ISaneMyDfsUtil & SaneMyDfsUtil

    从您作为示例GetDctmViewDefinitions 提供的一种方法开始,添加到新类并接口它使用GetObjectsWithContentMyDfsUtil 方法。新类上的这个“新”方法只是直接委托给现有的 - 且不可测试的 MyDfsUtil 类。您将此类的一个实例注入到被测类中。

    这样做有多种原因。

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

    1. 该类可能用于整个项目的各个代码级别。测试单个方法很快就会要求您模拟(详细)其中的几个方法。
    2. 该类变大了,需要重新分解为具有单一职责的不同类。您可以通过滚动位于MyDfsUtil 上的不同接口和类来做到这一点。随着时间的推移——当你有时间的时候——功能可以从MyDfsUtil 中出来并进入它实际所属的新类中。
    3. MyDfsUtil 中的方法可能会为您的用例返回太多。例如假设您正在测试的方法需要来自MyDfsUtil 的客户 ID 列表。你打电话给MyDfsUtil.QueryCustomers(myOrderId);,它会返回一个客户列表。您的代码只使用了客户的 Id 属性。在模拟该调用时,您必须创建客户对象、设置 id 并传回客户列表。在SaneMyDfsUtil 中,您可以有一个 返回客户ID 的QueryCustomerIds 方法。它使被测代码更加明确,并使测试的模拟更加简单。

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

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

    在你做太多其他事情之前阅读The Art of Unit Testing的最后一句话(老实说,买它并从头到尾阅读) 然后将Working Effectively with Legacy Code 放在你的桌子上,在它里面进进出出,并在遇到困难时将其作为参考。

    有什么问题直接喊

    【讨论】:

    • 很高兴知道不仅是我在为这些东西苦苦挣扎。我可以看到与您 3 年前的案例的相似之处。当我告诉他们我所做的事情时,我担心有人会生气(当然是之后)。至于你的问题,它实际上不是一个静态类。我认为找出这个类中到底存在多少混乱会很有趣。感谢您的阅读建议,一定会购买书籍(并阅读它们)!
    猜你喜欢
    • 2017-09-26
    • 1970-01-01
    • 1970-01-01
    • 2023-04-07
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多