一、几个概念
1.什么是外部依赖
外部依赖是指在系统中代码与其交互的对象,而且无法对其做人为控制。
最常见的例子是文件系统、线程、内存和时间等,我们使用桩对象来处理外部依赖问题。
2.什么是桩对象
桩对象是对系统中现有依赖的一个替代品,可人为控制。
通过使用桩对象,无需涉及依赖项,即可直接对代码进行测试。
3.什么是重构
重构是指不影响已有功能而改变代码设计的一种行为
4.什么是接缝
接缝是指代码中可以插入不同功能(如桩对象类)的地方。
二、解除依赖
抽象一个接口
namespace LogAn.Interface { public interface IExtensionManager { bool IsValid(string fileName); } }
实现接口的具体类
namespace LogAn.Implement { public class FileExtensionManager:IExtensionManager { public bool IsValid(string fileName) { if (string.IsNullOrEmpty(fileName)) { throw new ArgumentException("No filename provided!"); } if (!fileName.EndsWith(".SLF")) { return false; } else { return true; } } } }
编写一个实现该接口的桩对象类
无论文件的扩展类是什么,这个桩对象类永远返回true
public class StubExtensionManager:IExtensionManager { public bool IsValid(string fileName) { return true; } }
编写被测方法
现有一个接口和两个实现该接口的类,但被测类还是直接调用“真对象”;
这个时候我们需要在代码中引入接缝,以便可以使用桩对象
在被测试类中注入桩对象的实现;
在构造函数级别上接收一个接口;
namespace LogAn { public class LogAnalyzer { public bool IsValidLogFileName(string fileName) { IExtensionManager mgr =new FileExtensionManager(); return mgr.IsValid(fileName); } } }
三、在被测类中注入桩对象-构造函数
1.重写LogAnalyzer.cs
namespace LogAn { public class LogAnalyzer { private IExtensionManager manager; /// <summary> /// 在生产代码中新建对象 /// </summary> public LogAnalyzer() { manager = new FileExtensionManager(); } /// <summary> /// 定义可供测试调用的构造函数 /// </summary> /// <param name="mgr"></param> public LogAnalyzer(IExtensionManager mgr) { manager = mgr; } public bool IsValidLogFileName(string fileName) { return manager.IsValid(fileName); } } }
public class StubExtensionManager : IExtensionManager { public bool ShouldExtensionBeValid; public bool IsValid(string fileName) { return ShouldExtensionBeValid; } }
[TestFixture] public class LogAnalyzerTest { [Test] public void IsValidFileName_validFileLowerCased_ReturnTrue() { StubExtensionManager myFakeManager = new StubExtensionManager(); myFakeManager.ShouldExtensionBeValid = true; LogAnalyzer analyzer = new LogAnalyzer(myFakeManager); bool result = analyzer.IsValidLogFileName("haha.slf"); Assert.IsTrue(result, "filename shoud be valid!"); } }
4.构造函数注入方式存在的问题
如果被测代码需要多个桩对象才能正常工作,就需要增加更多的构造函数,而造成很大的困扰,甚至降低代码的可读性和可维护性
5.何时使用构造函数注入方式
使用构造函数的方式,可以很好的告知API使用者:“这些参数是必须的,新建这个对象时必须传入所有参数”
如果想要这些依赖变成可选的,可以使用属性注入
四、在被测类中注入桩对象-属性注入
1.重写LogAnalyzer.cs
namespace LogAn { public class LogAnalyzer { private IExtensionManager manager; /// <summary> /// 在生产代码中新建对象 /// </summary> public LogAnalyzer() { manager = new FileExtensionManager(); } /// <summary> /// 允许通过属性设置依赖 /// </summary> /// <param name="mgr"></param> public IExtensionManager ExtensionManager { get { return manager; } set { manager = value; } } public bool IsValidLogFileName(string fileName) { return manager.IsValid(fileName); } } }
public class StubExtensionManager : IExtensionManager { public bool ShouldExtensionBeValid; public bool IsValid(string fileName) { return ShouldExtensionBeValid; } }
[TestFixture] public class LogAnalyzerTest { [Test] public void IsValidFileName_validFileLowerCased_ReturnTrue() { StubExtensionManager myFakeManager = new StubExtensionManager(); myFakeManager.ShouldExtensionBeValid = true; LogAnalyzer analyzer = new LogAnalyzer(); analyzer.ExtensionManager = myFakeManager; bool result = analyzer.IsValidLogFileName("haha.slf"); Assert.IsTrue(result, "filename shoud be valid!"); } }
五、在被测类中注入桩对象-工厂方法
1.编写LogAnalyzer.cs
namespace LogAn { public class LogAnalyzer { private IExtensionManager manager; /// <summary> /// 在生产代码中使用工厂 /// </summary> /// <param name="mgr"></param> public LogAnalyzer() { manager = ExtensionManagerFactory.Create(); } public bool IsValidLogFileName(string fileName) { return manager.IsValid(fileName); } } }
2.编写测试方法
[TestFixture] public class LogAnalyzerTest { [Test] public void IsValidFileName_validFileLowerCased_ReturnTrue() { StubExtensionManager myFakeManager = new StubExtensionManager(); myFakeManager.ShouldExtensionBeValid = true; //把桩对象赋给工厂类 ExtensionManagerFactory.SetManager(myFakeManager); LogAnalyzer analyzer = new LogAnalyzer(); bool result = analyzer.IsValidLogFileName("haha.slf"); Assert.IsTrue(result, "filename shoud be valid!"); } }