• 单元测试(三)


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

      一.问题描述

          判断文件是否有效的需求变更了:有效的文件扩展名存储在文件系统中,要测试的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     /// 
     2     /// 扩展管理器工厂
     3     /// 
     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中方法就介绍完了,如有不对之处,请指出,大家共同学习!

  • 相关阅读:
    动手学深度学习PyTorch(六):卷积神经网络
    docker发布自己的镜像到docker hub上
    WIFI6 2.4G模组 WB800DC移植和替换RTL8723过程记录
    如何使用ZBrush颜色菜单,看完这篇文章你就会了
    k8s--基础--25.1--Helm--介绍和安装
    【第二章:Java开发岗:MySQL篇】
    结构体定义 typedef struct 用法详解和用法小结
    vue判断路由是否相等
    使用Node.js与Strve.js@4.3.0实战一款全新的群聊应用
    网安之PHP基础作业(5)
  • 原文地址:https://www.cnblogs.com/Tankard-tian/p/16041716.html