读书笔记-单元测试艺术(三)-使用桩对象解除依赖

一、几个概念

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);
        }
    }
}

2.编写桩对象

public class StubExtensionManager : IExtensionManager
 {
     public bool ShouldExtensionBeValid;
     public bool IsValid(string fileName)
     {
         return ShouldExtensionBeValid;
     }
 }

3.编写测试方法

[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);
        }
    }
}

2.编写桩对象类

public class StubExtensionManager : IExtensionManager
{
    public bool ShouldExtensionBeValid;
    public bool IsValid(string fileName)
    {
        return ShouldExtensionBeValid;
    }
}

3.编写测试方法

[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!");
    }
}
时间: 2024-08-01 21:48:11

读书笔记-单元测试艺术(三)-使用桩对象解除依赖的相关文章

读书笔记-单元测试艺术(一)-单元测试的基本知识

一.定义单元测试和集成测试 1.什么是单元测试 单元测试是一段自动化代码,用来调用被测试的方法或类,而后验证基于该方法或类的逻辑行为的一些假设. 单元测试几乎总是用单元测试框架来写的.它写起来很顺手,运行起来不费时,它是全自动的,可信赖的,可读性强的.可维护的. 2.什么是集成测试 集成测试(integration test)意味着把两个或多个相依赖的软件模块作为一组进行测试. 缺点:存在"意外缺陷",在代码修改后,如果不能对以前的功能运行测试,很可能会无意中破坏已有的功能.可通过&q

【字源大挪移—读书笔记】 第三部分:字尾

[字源大挪移—读书笔记] 第三部分:字尾 [3 字尾:[3.1]名词字尾.[3.2]形容词字尾.[3.3]副词字尾.[3.4]动词字尾 [3.1]名词字尾(Noun) [3.1.1]表示[人]的字尾 -ain -aire -an -ian -ean -ese -ant -ent -ary -ate -ee {[备注]:和-er相反,表示"被……的人":} -eer -er -or -ar -ier -eur -ician -ist -ite -ive -man -on -ster -y

深度理解java虚拟机读书笔记(二)HotSpot Java对象创建,内存布局以及访问方式

内存中对象的创建.对象的结构以及访问方式. 一.对象的创建 在语言层面上,对象的创建只不过是一个new关键字而已,那么在虚拟机中又是一个怎样的过程呢? (一)判断类是否加载.虚拟机遇到一条new指令的时候,首先会检查这个指令的参数是否能在常量池中定位到一个类的符号引用,并且检查这个符号代表的类是否被加载.解析并初始化.如果没有完成这个过程,则必须执行相应类的加载. (二)在堆上为对象分配空间.对象需要的空间大小在类加载完成后便能确定.之后便是在堆上为该对象分配固定大小的空间.分配的方式也有两种:

Effective C++读书笔记之十二:复制对象时勿忘其每一个成分

Item 12:Copy all parts of an object 如果你声明自己的copying函数,意思就是告诉编译器你并不喜欢缺省显示中的某些行为.而编译器会对"你自己写出copying函数"做出一种复仇的行为:既然你拒绝它们为你写出copying函数,如果你的代码不完全,它们也不会告诉你.结论很明显:如果你为class添加一个成员变量,你必须同时修改copying函数.如果你忘记,编译器不太可能提醒你. 一下提供一种正确的模版: class Date{...}; class

读书笔记-APUE第三版-(7)进程环境

本章关注单进程运行环境:启动&终止.参数传递和内存布局等. 进程启动终止 如图所示: 启动:内核通过exec函数执行程序,在main函数运行之前,会调用启动例程(start-up routine),取得命令行参数和环境变量.可以把启动例程理解为exit(main(argc,argv)). 终止:五种正常终止方式(从main方法返回/exit/_exit/最后一个线程返回/最后一个线程退出):三种异常终止方式(abort/接收到信号/最后一个线程接收到取消请求). exit与_exit关系:exi

linq读书笔记2-查询内存中的对象

上次我们说到了linq对数组内容的检索,自.net2.0以后,泛型成了很常见的一种应用技术,linq对泛型的检索也提供了完善的支持 如对list类型的支持,范例如下: class Program    {        static void Main(string[] args)        {            List<Books> samplebooks = new List<Books>() {                                    

读书笔记-APUE第三版-(5)标准IO库

ISO C标准I/O库使用流的概念读写文件.流是对数据传输的抽象,可以把流理解为从起点到终点间的字节序列. 标准I/O库通过维护进程空间内的缓冲区,减少read/write系统调用次数来提高I/O效率.之前介绍的Unbuffered I/O和文件描述符fd打交道,标准I/O则使用FILE指针. typedef struct{ short level;/*缓冲区满程度*/ unsigned flags;/*文件打开状态标志*/ char fd;/*文件描述符*/ unsigned char hol

读书笔记-APUE第三版-(8)进程控制

进程ID 每一个进程都有一个唯一的进程ID.几个特殊进程: 0号进程是内核进程,一般是调度进程swapper. 1号进程init,是用户进程(以root权限执行/sbin/init),负责初始化. 几个重要函数:getpid(进程ID)/getppid(父进程ID)/getuid(进程真有用户ID)/geteuid(进程有效用户ID)/getgid(进程真有用户组ID)/getegid(进程有效用户组ID). fork/exec/wait例程 fork家族函数用于创建子进程(父子进程关系下节详细

读书笔记-APUE第三版-(6)系统数据文件和信息

常见系统数据文件 下表列出了常见的系统数据文件及其查找函数. 以/etc/passwd文件为例,读取数据的程序基本框架如下: void get_pw_entry() { struct passwd *ptr; setpwent(); while ((ptr = getpwent()) != 0) { -- } endpwent(); return ptr; } 每个数据文件都提供了一个get方法返回文件下一个记录项. set方法充值当前位置到文件开始处. end方法关闭数据文件. 表格中的get