[控制反转]以Mock进行Unit Test为例
大家写程序写一阵子以后就会开始听到一些名词
控制反转(Inversion of Control,缩写为IoC)
相依性注入(Dependency Injection,简称DI)
也许再加上很久以前也许就没认真搞懂的界面 (Interface)
关于这些东西到底是什么意思 我想中文解释大家都会背
但我可能就是搞不懂 到底把程序搞这么复杂有什么好处?
原本的程序也跑得很好 为什么大家总是说得这样写才好?
我想用最简单的例子来做个说明
假设我们的程序分三层
DB → (1).Repository →(2).Service → (3).实际程序
也就是说 (3).实际程序 不能跳过 (2).Service 直接取 (1).Repository
更不可能跳过 (2). Service + (1).Repository 直接碰DB
(1). Repository 实际提供数据的一层
=>不管你是要写SQL、Stored Procedure、ORM...... 反正你数据怎取出就是在这层
(2). Service 商业逻辑
=> 我随便假设一个例子 我的真实姓名叫'陈大宝' (存在DB里) 但因为个资法的关系 我对外只能显示 '陈'
(3). 实际程序
=> 这不用解释吧...
我们的题目如下
DB里有一张会员基本数据表 里面存了会员的个资
其中名子的部分 我希望只显示姓 (两个星号)
为了讲解方便
我把DB、(1). Repository、(2). Service、(3). 实际程序,通通放在同一个文件,并且通通都设public
实际上他们通常会分散在不同cs档、或不同项目中、并且不会通通是public,这点请特别注意
我们很快的可以完成第一版的程序
using System;
using System.Collections.Generic;
using System.Linq;
namespace MySample.Console
{
public class Sample
{
public int Id { get; set; }
public string Name { get; set; }
public DateTime Birthday { get; set; }
}
///
/// 实际存取DB
///
public class Repository
{
public List GetSample(int param)
{
//为了方便讲解 (否则这些数据应该是从 DB 取出)
var source = new List
{
new Sample { Id = 1, Name = "陈大宝", Birthday = DateTime.Parse("2001/1/1") },
new Sample { Id = 2, Name = "林小宝", Birthday = DateTime.Parse("2001/2/1") },
new Sample { Id = 3, Name = "王聪明", Birthday = DateTime.Parse("2001/3/1") },
};
return source.Where(q => q.Id == param).ToList();
}
}
///
/// 透过Repository从DB取数据后 做一些商业逻辑
///
public class Service
{
Repository repo = new Repository();
public List GetSample(int param)
{
var samples = repo.GetSample(param);
//假设有一个商业逻辑是大家不能知道会员的全名 只能知道 姓氏 +
foreach (var item in samples)
{
if (item.Name != null && item.Name.Length >=1 )
{
item.Name = item.Name.Substring(0, 1) + "";
}
}
return samples;
}
}
///
/// DB → Repository → Service → 实际程序
///
class Program
{
static void Main(string[] args)
{
Service service = new Service();
var result = service.GetSample(1);
}
}
}
这程序很完美的解决了我们的问题
但我们从程序中发现
1. 实际程序 相依于 Service ... 第一行我就 new Service()
2. Service 相依于 Repository ... 第一行我就 new Repository()
这程序肯定不是什么控制反转 也不会是什么相依性注入 但那又如何 我的程序依旧跑的很好 没造成困扰
而故事的进行肯定会有些转折
某天有人反应 你的程序是不是写错了?
说好要显示陈 怎么昨天从半夜开始 页面通通变成陈大宝了 !?!?!?!?
oh 那可不得了
秀出完整姓名等于违反个资法 我得马上测一下这段程序有没有问题
但你观察了一下这段程序 你该从何测起?
1. Repository 相依于 DB
=> 这倒简单 我从config里 改一改连线参数 指到我的测试DB 再手动塞一些假数据即可
2. Service 相依于 Repository
=> 嗯... 好像暂时想不到该怎么测... 但没关系 反正现在问题也不在这里 并不是输入'陈大宝' 却取出 '林小宝' 的数据
3. 实际程序 相依于 Service
=> 这就是出问题的地方!!! 把名子变成的程序就在这!!
虽然暂时还没办法想出所有答案 但至少现在我们已经有解决的办法了
解法如下:
1. 到测试DB做一笔假数据 ( Id=1、Name=陈大宝) 然后把config的连线字符串改成测试DB
2. 从实际程序输入参数 Id=1、跑一次Service.GetSample()
便可知道是哪里出错!
但为什么这样可以让测试成立呢?
原来 Repository 相依于 DB
但 Repository 是透过 连线字符串 去开DB
故注入 Repository 的实体 其实就是 连线字符串(放在config中)
所以当我修改了连线字符串 => 传进Repository里的东西不一样了 => 让正式数据库 变成 测试数据库
即 Repository→连线字符串 (可人为操作)→DB ...是因为这样的设计让我们可以操控 config 进行测试
那如果很不幸的 今天你换到测试机以后 发现输出结果真的是陈大宝 而非陈
但该死的 这一个月来有10个PG 进了20个版本 程序变成10万行
public class Repository
{
public List GetSample(int param)
{
//多了两百行程序
//好像又多开 4 张 Table
//我也看不懂他到底在串什么
//该不会其实是这里坏掉吧?
return source.Where(q => q.Id == param).ToList();
}
}
public List GetSample(int param)
{
//最近上版10次增加了500行!!!!
var samples = repo.GetSample(param);
//为什么这里又要去call一些我觉得根本用不到的函数?
foreach (var item in samples)
{
if (item.Name != null && item.Name.Length >=1 )
{
item.Name = item.Name.Substring(0, 1) + "";
//做这些运算不是没意义吗? 为什么这里又多了50行???????
}
}
//上次他们好像说有个项目要加新功能 所以这里的100行是在搞那个功能?????
return samples;
}
虽然我透过切换测试DB 确定程序真的被改坏了
但我只能确定问题不在DB (因为DB是我连到测试环境手动造假 ( Mock ) 的)
那问题到底是Repository最近上版被改坏? 还是Service被改坏?
那我有没有办法像刚刚改连线字符串一样 先再排除一个可能?
我可以手动做一个已知的 肯定不会错的 Repository层 吗?
就像刚刚我自己手 key 测试 DB 一样 让我再排除一个可能的选项吗?
于是我们来看看第二版的程序
先到Nuget下载Moq
改变的部分如下
Repository层
Service层
我的程序
测试程序
完整程序如下
using Moq;
using System;
using System.Collections.Generic;
using System.Linq;
namespace MySample.Console
{
public class Sample
{
public int Id { get; set; }
public string Name { get; set; }
public DateTime Birthday { get; set; }
}
public interface IRepository
{
List GetSample(int param);
}
///
/// 实际存取DB
///
public class Repository : IRepository
{
public List GetSample(int param)
{
//为了方便讲解 (否则这些数据应该是从 DB 取出)
var source = new List
{
new Sample { Id = 1, Name = "张小华", Birthday = DateTime.Parse("2001/1/1") },
new Sample { Id = 2, Name = "林小宝", Birthday = DateTime.Parse("2001/2/1") },
new Sample { Id = 3, Name = "王聪明", Birthday = DateTime.Parse("2001/3/1") },
};
return source.Where(q => q.Id == param).ToList();
}
}
///
/// 透过Repository从DB取数据后 做一些商业逻辑
///
public class Service
{
IRepository repo;
public Service(IRepository pRepo)
{
repo = pRepo;
}
public List GetSample(int param)
{
var samples = repo.GetSample(param);
//假设有一个商业逻辑是大家不能知道会员的全名 只能知道 姓氏 +
foreach (var item in samples)
{
if (item.Name != null && item.Name.Length >=1 )
{
item.Name = item.Name.Substring(0, 1) + "";
}
}
return samples;
}
}
///
/// DB → Repository → Service → 实际程序
///
class Program
{
static void Main(string[] args)
{
Service service = new Service(new Repository());
var result = service.GetSample(1);
var test = MyTest();
System.Diagnostics.Debug.WriteLine("Hello World!");
}
///
/// 假设这是一个test case
///
///
public static bool MyTest()
{
//arrange
var source = new List
{
new Sample { Id = 1, Name = "陈大宝", Birthday = DateTime.Parse("2001/1/1") },
};
var mockRepo = new Mock();
mockRepo.Setup(repo => repo.GetSample(It.IsAny())).Returns(source);
var service = new Service(mockRepo.Object);
//act
var actual = service.GetSample(1);
//assert
return actual.First().Name == "陈";
}
}
}
最后我们整理了一下刚刚发生什么事
连线字符串config的发想
A. 若来源是可以被抽换的 ( 正式DB 与 测试DB ) => 控制反转
B. 透过config的变更 => 相依性注入
C. 因为真假DB都是一模一样的 所以能达到一行程序都不用改 也能验证程序对不对
=>正式DB 与 测试DB 的 Table Schema 一模一样
=>继承同一界面 IRepo 的 真Repo (原程序) 与 假Repo (Mock) 结构也一模一样
=>所以程序才会一行都不用改
总结一下
在要测试的程序 (Service) 中
会变的变因 (Repository) 都被我控制住了
所以当结果出错时 ( 输出 陈大宝 而非 陈 )
我便可确定 Service 有错
因为 Repository 从头到尾就根本没跑 他是一个被我控制住的变因 是我自己造假结果 ( Mock ) 以后传进去的
从头到尾就只有Service在变 所以若结果出错 则 Service 必然有问题
这也就是控制反转、相依性注入之所以有意义的地方
原文:大专栏 [控制反转]以Mock进行Unit Test为例
原文地址:https://www.cnblogs.com/petewell/p/11516367.html
- 关于CPU漏洞Spectre的详细分析
- 17.2 准备工作
- 克隆虚拟机的注意点
- keepalived+nginx搭建高可用(注意点)
- 我的WCF之旅(10):如何在WCF进行Exception Handling
- 安装nginx出现的问题
- 18.11 LVS DR模式搭建
- Linux基础(day64)
- 我的WCF之旅(9):如何在WCF中使用tcpTrace来进行Soap Trace
- 物联网设备已沦陷,咖啡机也不能例外
- 我的WCF之旅(13):创建基于MSMQ的Responsive Service
- 开发自己的Data Access Application Block[上篇]
- 18.9/18.10 LVS NAT模式搭建
- 谈谈WCF中的Data Contract (1):Data Contract Overview
- JavaScript 教程
- JavaScript 编辑工具
- JavaScript 与HTML
- JavaScript 与Java
- JavaScript 数据结构
- JavaScript 基本数据类型
- JavaScript 特殊数据类型
- JavaScript 运算符
- JavaScript typeof 运算符
- JavaScript 表达式
- JavaScript 类型转换
- JavaScript 基本语法
- JavaScript 注释
- Javascript 基本处理流程
- Javascript 选择结构
- Javascript if 语句
- Javascript if 语句的嵌套
- Javascript switch 语句
- Javascript 循环结构
- Javascript 循环结构实例
- Javascript 跳转语句
- Javascript 控制语句总结
- Javascript 函数介绍
- Javascript 函数的定义
- Javascript 函数调用
- Javascript 几种特殊的函数
- JavaScript 内置函数简介
- Javascript eval() 函数
- Javascript isFinite() 函数
- Javascript isNaN() 函数
- parseInt() 与 parseFloat()
- escape() 与 unescape()
- Javascript 字符串介绍
- Javascript length属性
- javascript 字符串函数
- Javascript 日期对象简介
- Javascript 日期对象用途
- Date 对象属性和方法
- Javascript 数组是什么
- Javascript 创建数组
- Javascript 数组赋值与取值
- Javascript 数组属性和方法
- python读取图像矩阵文件并转换为向量实例
- PHP echo()函数讲解
- Python3开发环境搭建详细教程
- php使用QueryList轻松采集js动态渲染页面方法
- PHP convert_uudecode()函数讲解
- php实现在线考试系统【附源码】
- 实例介绍PHP中zip_open()函数用法
- php实现数字补零的方法总结
- PHP配置ZendOpcache插件加速
- 详解php用static方法的原因
- phpinfo无法显示的原因及解决办法
- 在php的yii2框架中整合hbase库的方法
- PHP安装memcache扩展的步骤讲解
- python退出循环的方法
- PHP crypt()函数的用法讲解