桩破除依赖2-构造函数注入

在构造函数层注入一个伪对象(构造函数注入)

代码地址:http://git.oschina.net/zhv/UnitTest

这个方法需要给被测试类添加一个新的构造函数,或给已有的构造函数添加一个新的参数,传入一个之前抽取出来的接口(IExtensionManager)类型对象。然后在被测试类中添加一个这个接口类型的局部字段,把传入的对象付给这个局部字段,供被测试方法或其他方法使用。

ClassUnderTest(IExtensionManager mgr) 

{ m_manager = mgr }

IExtensionManager m_manager;

IsValidFileName(string)
{
    if(m_manager.isvalid(file))
    ....
}

以下是构造注入的代码

class LogAnalyzerConstructorInject
{
    //定义局部字段
    private IExtensionManager manager;

    //定义测试代码可以调用的构造函数
    public LogAnalyzerConstructorInject(IExtensionManager mgr)
    {
        manager = mgr;
    }

    public bool IsValidLogFileName(string fileName)
    {
        //使用构造函数传入的IExtensionManager类
        return manager.IsValid(fileName);
    }
}

public interface IExtensionManager
{
    bool IsValid(string fileName);
}

[TestFixture]
public class LogAnalyzerTests
{
    [Test]
    public void IsValidFileName_NameSupportedExtension_ReturnsTrue()
    {
        //准备一个返回true的桩
        FakeExtensionManager myFakeManager =
            new FakeExtensionManager();
        myFakeManager.WillBeValid = true;

        //传入桩
        LogAnalyzerConstructorInject log =
            new LogAnalyzerConstructorInject(myFakeManager);
        bool result = log.IsValidLogFileName("short.ext");
        Assert.True(result);
    }
}

//定义一个最简单的桩
internal class FakeExtensionManager : IExtensionManager
{
    public bool WillBeValid = false;

    public bool IsValid(string fileName)
    {
        return WillBeValid;
    }
}

使用构造函数注入伪对象可能会带来问题:

  • 如果被测试代码需要放置多个桩才能在没有依赖项的情况下正常工作,加入越来越多的构造函数或越来越多的构造函数参数,就变得很困难,还会降低代码可读性和可维护性。

    • public LogAnalyzer(IExtensionManager mgr,Ilog logger,IWebService service)这样多的参数会降低类的可维护性
  • 解决方法是创建一个特殊类,包含初始化一个类的所有值。依赖项多的话,这个类还是会失控。

  • 一个方案是使用控制反转(Inversion of Control IoC)容器。

用伪对象模拟异常

首先给之前的FakeExtensionManager增加抛出异常的桩,然后再添加测试代码,注入代码中。 如果需要测试通过,就需要在被测试代码

//测试抛出异常的情况
[Test]
public void IsValidFileName_ExtManagerThrowsException_ReturnFalse()
{
    FakeExtensionManager myFakeManager = new FakeExtensionManager();
    myFakeManager.WillThrow = new Exception("This is fake");

    LogAnalyzerConstructorInject log =
        new LogAnalyzerConstructorInject(myFakeManager);
    bool result = log.IsValidLogFileName("anything.anyextension");
    Assert.False(result);
}

//定义一个最简单的桩,具有返回true、false和抛出异常的功能
internal class FakeExtensionManager : IExtensionManager
{
    //模拟返回true或false用的,需要测试时赋值
    public bool WillBeValid = false;

    //模拟返回异常的,需要测试时赋值
    public Exception WillThrow = null;
    public bool IsValid(string fileName)
    {
        if (WillThrow != null) { throw WillThrow; }

        return WillBeValid;
    }
}

增加了try-catch的被测代码

class LogAnalyzerConstructorInject
{
    //定义局部字段
    private IExtensionManager manager;

    //定义测试代码可以调用的构造函数
    public LogAnalyzerConstructorInject(IExtensionManager mgr)
    {
        manager = mgr;
    }

    public bool IsValidLogFileName(string fileName)
    {
        ////使用构造函数传入的IExtensionManager类
        //return manager.IsValid(fileName);

        //给上面代码增加try-catch块
        try
        {
            return manager.IsValid(fileName);
        }
        catch
        {
            return false;
        }
    }
}

使用构造函数注入的时机

如果想告诉API的使用者某些参数是必须的,在构造函数中使用这些参数是一个极好的办法,这些参数必须在创建对象时传入。 而如果想让这些依赖成为可选项,就可以使用属性注入的方法

相关文章

迭代器模式(Iterator)迭代器模式(Iterator)[Cursor]意图...
高性能IO模型浅析服务器端编程经常需要构造高性能的IO模型,...
策略模式(Strategy)策略模式(Strategy)[Policy]意图:定...
访问者模式(Visitor)访问者模式(Visitor)意图:表示一个...
命令模式(Command)命令模式(Command)[Action/Transactio...
生成器模式(Builder)生成器模式(Builder)意图:将一个对...