[控制反转]以Mock进行Unit Test为例

时间:2019-09-13
本文章向大家介绍[控制反转]以Mock进行Unit Test为例,主要包括[控制反转]以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