单元测试(三)

当要测试的对象依赖另一个无法控制的对象(系统相关、第三方服务等),这个时候我们应该如何测试?
一.问题描述
判断文件是否有效的需求变更了:有效的文件扩展名存储在文件系统中,要测试的FileVerify类就依赖FileExtensionManager类,在这种场景下如何测试FileVerify类的逻辑呢?

1public class FileVerify 2{ 3public bool IsValidFileName(string fileName) 4{ 5FileExtensionManager manager = new FileExtensionManager(); 7return manager.IsValid(fileName); 8} 9} 10 11public class FileExtensionManager 12{ 13public bool IsValid(string fileName) 14{ 15//从文件系统中读取文件并判断 16return true; 17} 18}

二.破除依赖的3种解决方案
方法1.对被测试类继承并重写某些行为(最简单的一种方法 无需引入新的接口和实现类)该方法在简单的同时也同时失去了对被被测试代码更多的控制空间,也就时说能做的事情是有限的
修改被测试代码,将IsValid方法定义为virtual,这样子类就可以重写该方法并决定该方法返回的结果
1public class FileVerify 2{ 3public bool IsValidFileName(string fileName) 4{ 5return IsValid(fileName); 6} 7 8 public virtual bool IsValid(string fileName) 9{ 10 FileExtensionManager manager = new FileExtensionManager(); 11 return manager.IsValid(fileName); 12} 13}

在测试类中创建FileVerify的子类TestFileVerity并修改测试类
1internal class TestFileVerify :FileVerify 2{ 3public bool IsSupported { get; set; } 4 5public override bool IsValid(string fileName) 6{ 7return IsSupported; 8} 9} 10 11[TestFixture] 12 public class FileVerifyTests 13{ 14[Test] 15 public void IsValidFileName_NameSupportedExtension_RetureTrue() 16{ 17 TestFileVerify fileVerify = new TestFileVerify(); 18 fileVerify.IsSupported = true; 19 20 bool result = fileVerify.IsValidFileName("test.txt"); 21 22Assert.IsTrue(result); 23} 24 }

注意:以下方法均需在被测试项目中定义接口IExtensionManager,并在测试项目中添加一个接口的实现类FakeExtensionManager,下面为具体代码:
1public interface IExtensionManager 2{ 3bool IsValid(string fileName); 4} 5 6public class FileExtensionManager : IExtensionManager 7{ 8public bool IsValid(string fileName) 9{ 10//从文件系统中读取文件并判断 11return true; 12} 13}

1internal class FakeExtensionManager : IExtensionManager 2{ 3public bool WillBeValid { get; set; } 4 5public bool IsValid(string fileName) 6{ 7return WillBeValid; 8} 9}


方法2 :继承被测试类并重写方法(与方法1相比 需引入接口与测试实现类)被测试类代码修改代码如下
1public class FileVerify 2{ 3public bool IsValidFileName(string fileName) 4{ 5return GetManager().IsValid(fileName); 6} 7 8 public virtual IExtensionManager GetManager() 9{ 10 return new FileExtensionManager(); 11} 12 }

在测试类中创建FileVerify的子类TestFileVerity并修改测试类
1internal class TestFileVerify :FileVerify 2{ 3public TestFileVerify(IExtensionManager manager) 4{ 5this.manager = manager; 6} 7 8 private readonly IExtensionManager manager; 9 10 public override IExtensionManager GetManager() 11{ 12 return manager; 13} 14} 15 16[TestFixture] 17 public class FileVerifyTests 18{ 19[Test] 20 public void IsValidFileName_NameSupportedExtension_RetureTrue() 21{ 22 FakeExtensionManager manager = new FakeExtensionManager(); 23 manager.WillBeValid = true; 24 TestFileVerify fileVerify = new TestFileVerify(manager); 25 26 bool result = fileVerify.IsValidFileName("test.txt"); 27 28Assert.IsTrue(result); 29} 30 } 


方法3:该方法的思路是被测试类依赖于接口,不依赖于具体的实现 那么问题就转换成如何给被测试类传入具体的依赖项对于这个思路有几个解决方案
①构造函数注入
1public class FileVerify 2{ 3public FileVerify(IExtensionManager manager) 4{ 5this.manager = manager; 6} 7 8private readonly IExtensionManager manager; 9 10public bool IsValidFileName(string fileName) 11{ 12return manager.IsValid(fileName); 13} 14}

使用构造函数注入需注意:
  • 在只有一个构造函数的情况下,这个类的所有使用者都必须传入依赖
  • 当这个类还需其它的依赖,例如日志服务、Web服务,那么构造函数中会加入更多的参数,会降低可读性和可维护性;解决这种情况有两种方案:①创建一个特殊类,将创建这个类所需依赖的类型作为属性,而构造函数中只有一个参数,就是这个特殊类②使用第三方Ioc容器来管理依赖
②属性注入
1public class FileVerify 2{ 3public FileVerify() 4{ 5manager = new FileExtensionManager(); 6} 7 8public IExtensionManager Manager 9{ 10get => manager; 11set => manager = value; 12} 13 14private IExtensionManager manager; 15 16public bool IsValidFileName(string fileName) 17{ 18return manager.IsValid(fileName); 19} 20}

使用属性注入要比使用构造函数注入比较简单,每个测试只需要设置自己需要设置的属性
③使用工厂,从工厂类中获得实例,在被测试项目中创建工厂类的代码如下:
1/// 2/// 扩展管理器工厂 3/// 4internal class ExtensionManagerFactory 5{ 6private static IExtensionManager _manager; 7 8public static IExtensionManager Create() 9{ 10if(_manager != null) 11{ 12return _manager; 13} 14return new FileExtensionManager(); 15} 16 17public static void SetManager(IExtensionManager manager) 18{ 19_manager = manager; 20} 21}

被测试类代码如下:在构造函数中通过工厂类创建默认的实例
1public class FileVerify 2{ 3public FileVerify() 4{ 5manager = ExtensionManagerFactory.Create(); 6} 7 8private IExtensionManager manager; 9 10public bool IsValidFileName(string fileName) 11{ 12return manager.IsValid(fileName); 13} 14}

测试代码如下:
1[TestFixture] 2public class FileVerifyTests 3{ 4[Test] 5public void IsValidFileName_NameSupportedExtension_RetureTrue() 6{ 7FakeExtensionManager manager = new FakeExtensionManager(); 8manager.WillBeValid = true; 9ExtensionManagerFactory.Create(); 10FileVerify fileVerify = new FileVerify(); 11 12bool result = fileVerify.IsValidFileName("test.txt"); 13 14Assert.IsTrue(result); 15} 16}

方法2和方法3比方法1相对来说比较麻烦,因为引入了新的接口,新的实现,引入了工厂方法,但是这两种方法的可控制空间比较大,例如,可以在FakeExtensionManager类中模拟异常
【单元测试(三)】破除依赖的3中方法就介绍完了,如有不对之处,请指出,大家共同学习!

    推荐阅读