单元测试(三)

  当要测试的对象依赖另一个无法控制的对象(系统相关、第三方服务等),这个时候我们应该如何测试?

  一.问题描述

      判断文件是否有效的需求变更了:有效的文件扩展名存储在文件系统中,要测试的FileVerify类就依赖FileExtensionManager类,在这种场景下如何测试FileVerify类的逻辑呢? 

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

  二.破除依赖的3种解决方案

   方法1.对被测试类继承并重写某些行为(最简单的一种方法 无需引入新的接口和实现类)该方法在简单的同时也同时失去了对被被测试代码更多的控制空间,也就时说能做的事情是有限的

        修改被测试代码,将IsValid方法定义为virtual,这样子类就可以重写该方法并决定该方法返回的结果

 1    public class FileVerify
 2     {
 3         public bool IsValidFileName(string fileName)
 4         {
 5             return 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并修改测试类

 1     internal class TestFileVerify :FileVerify
 2     {
 3         public bool IsSupported { get; set; }
 4 
 5         public override bool IsValid(string fileName)
 6         {
 7             return 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 
22             Assert.IsTrue(result);
23         }
24     }

   注意:以下方法均需在被测试项目中定义接口IExtensionManager,并在测试项目中添加一个接口的实现类FakeExtensionManager,下面为具体代码:

 1     public interface IExtensionManager
 2     {
 3         bool IsValid(string fileName);
 4     }
 5 
 6     public class FileExtensionManager : IExtensionManager
 7     {
 8         public bool IsValid(string fileName)
 9         {
10             //从文件系统中读取文件并判断
11             return true;
12         }
13     }
1     internal class FakeExtensionManager : IExtensionManager
2     {
3         public bool WillBeValid { get; set; }
4 
5         public bool IsValid(string fileName)
6         {
7             return WillBeValid;
8         }
9     }

  

  方法2 :继承被测试类并重写方法(与方法1相比 需引入接口与测试实现类)被测试类代码修改代码如下  

 1     public class FileVerify
 2     {
 3         public bool IsValidFileName(string fileName)
 4         {
 5             return GetManager().IsValid(fileName);
 6         }
 7 
 8         public virtual IExtensionManager GetManager()
 9         {
10             return new FileExtensionManager();
11         }
12     }

         在测试类中创建FileVerify的子类TestFileVerity并修改测试类

 1     internal class TestFileVerify :FileVerify
 2     {
 3         public TestFileVerify(IExtensionManager manager)
 4         {
 5             this.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 
28             Assert.IsTrue(result);
29         }
30     } 

 

  方法3:该方法的思路是被测试类依赖于接口,不依赖于具体的实现 那么问题就转换成如何给被测试类传入具体的依赖项  对于这个思路有几个解决方案

  ①构造函数注入

 1     public class FileVerify
 2     {
 3         public FileVerify(IExtensionManager manager)
 4         {
 5             this.manager = manager;
 6         }
 7 
 8         private readonly IExtensionManager manager;
 9 
10         public bool IsValidFileName(string fileName)
11         {
12             return manager.IsValid(fileName);
13         }
14     }

   使用构造函数注入需注意:

  • 在只有一个构造函数的情况下,这个类的所有使用者都必须传入依赖
  • 当这个类还需其它的依赖,例如日志服务、Web服务,那么构造函数中会加入更多的参数,会降低可读性和可维护性;解决这种情况有两种方案:①创建一个特殊类,将创建这个类所需依赖的类型作为属性,而构造函数中只有一个参数,就是这个特殊类  ②使用第三方Ioc容器来管理依赖      

  ②属性注入

 1     public class FileVerify
 2     {
 3         public FileVerify()
 4         {
 5             manager = new FileExtensionManager();
 6         }
 7 
 8         public IExtensionManager Manager 
 9         {
10             get => manager;
11             set => manager = value;
12         }
13 
14         private IExtensionManager manager;
15 
16         public bool IsValidFileName(string fileName)
17         {
18             return manager.IsValid(fileName);
19         }
20     }

  使用属性注入要比使用构造函数注入比较简单,每个测试只需要设置自己需要设置的属性

  ③使用工厂,从工厂类中获得实例,在被测试项目中创建工厂类的代码如下:  

 1     /// <summary>
 2     /// 扩展管理器工厂
 3     /// </summary>
 4     internal class ExtensionManagerFactory
 5     {
 6         private static IExtensionManager _manager;
 7 
 8         public static IExtensionManager Create()
 9         {
10             if(_manager != null)
11             {
12                 return _manager;
13             }
14             return new FileExtensionManager();
15         }
16 
17         public static void SetManager(IExtensionManager manager)
18         {
19             _manager = manager; 
20         }
21     }

  被测试类代码如下:在构造函数中通过工厂类创建默认的实例  

 1     public class FileVerify
 2     {
 3         public FileVerify()
 4         {
 5             manager = ExtensionManagerFactory.Create();
 6         }
 7 
 8         private IExtensionManager manager;
 9 
10         public bool IsValidFileName(string fileName)
11         {
12             return manager.IsValid(fileName);
13         }
14     }

  测试代码如下:

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

  方法2和方法3比方法1相对来说比较麻烦,因为引入了新的接口,新的实现,引入了工厂方法,但是这两种方法的可控制空间比较大,例如,可以在FakeExtensionManager类中模拟异常

  破除依赖的3中方法就介绍完了,如有不对之处,请指出,大家共同学习!