.net测试篇之Moq框架简单使用

系列目录html

Moq库简介及安装

Moq简介

Moq是.net平台下的一个很是流行的模拟库,只要有一个接口它就能够动态生成一个对象,底层使用的是Castle的动态代理功能.数据库

它的流行赖于依赖注入模式的兴起,如今愈来愈多的分层架构使用依赖注入的方式来解耦层与层之间的关系.最为常见的是数据层和业务逻辑层之间的依赖注入,业务逻辑层再也不强依赖数据层对象,而是依赖数据层对象的接口,在IOC容器里完成依赖的配置.服务器

这种解耦给单元测试带来了巨大的便利,使得对业务逻辑的测试能够脱离对数据层的依赖,单元测试的粒度更小,更容易排查出问题所在.网络

你们可能都知道,数据层的接口每每有不少方法,少则十几个,多则几十个.咱们若是在单元测试的时候把接口切换为假实现,即便实现类全是空也须要大量代码,而且这些代码不可重用,一旦接口层改变不但要更改真实数据层实现还要修改这些专为测试作的假实现.这显然是不小的工做量.架构

幸亏有Moq,它能够在编译时动态生成接口的代理对象.大大提升了代码的可维护性,同时也极大减小工做量.框架

除了动态建立代理外,Moq还能够进行行为测试,触发事件等.函数

Moq安装

Moq安装很是简单,在Nuget里面搜索moq,第一个结果即是moq框架,点击安装便可.工具

Moq简单使用

本示例中要使用到的代码以下单元测试

public class MyDto
    {
        public string Name { get; set; }
        public int Age { get; set; }
    }
    public interface IDataBaseContext<out T> where T:new()
    {
        T GetElementById(string id);
        IEnumerable<T> GetAll();
        IEnumerable<T> GetElementsByName(string name);
        IEnumerable<T> GetPageElementsByName(string name, int startPage = 0, int pageSize = 20);
        IEnumerable<T> GetElementsByDate(DateTime? startDate, DateTime? endDate);
    }

    public class MyBll
    {
        private readonly IDataBaseContext<MyDto> _dataBaseContext;

        public MyBll(IDataBaseContext<MyDto> dataBaseContext)
        {
            _dataBaseContext = dataBaseContext;
        }

        public MyDto GetADto(string id)
        {
            if (string.IsNullOrWhiteSpace(id)) return null;
            return _dataBaseContext.GetElementById(id);
        }
    }

MyDto为业务层和数据层交互的对象,IDataBaseContext为数据层接口,MyBll为咱们的业务逻辑层测试

咱们要测试的是业务逻辑层的代码.这里业务逻辑类并无无参构造函数,若是手动建立起来很是麻烦,里面的坑前面说过.下面看如何使用Moq来模拟一个IDataBaseContext对象

咱们编写如下测试类

[Test]
        public void SimpleTest()
        {
            var moq = new Mock<IDataBaseContext<MyDto>>();
            MyBll bll = new MyBll(moq.Object);
            var result = bll.GetADto(null);
            Assert.Null(result);
        }

因为bll的GetADto若是传的参数是null或者空就会返回一个null对象,因些返回的结果是Null,以上测试会经过.

这里咱们首先建立了一个moq对象,它的Object属性就是咱们要模拟的IDataBaseContext 对象,咱们在建立MyBll对象时把它做为参数传入.

Moq基本配置

咱们再为MyBll添加如下方法

public IEnumerable<MyDto> GetDtos(string name)
        {
            if (string.IsNullOrWhiteSpace(name)) return null;
            var dtos = _dataBaseContext.GetElementsByName(name);
           return dtos;
        }

咱们编写以下测试方法

[Test]
        public void ShouldReturn_A_Collection_Of_Dtos()
        {
            var moq = new Mock<IDataBaseContext<MyDto>>();
            MyBll bll = new MyBll(moq.Object);
            var dtos = bll.GetDtos("sto");
        }

以上测试方法调用了bll的GetDtos方法,咱们知道GetDtos内部调用了数据访问接口的GetElementsByName方法,咱们在调试模式下看看返回的结果是什么.

Avatar

它返回了一个空集合,实际上无论咱们提供的是什么样的字符串,它都返回一个空集合,这是默认行为,由于_dataBaseContext.GetElementsByName并不知道咱们的真实逻辑是什么.

这样很显然并非总能知足咱们的要求,不少时候咱们在测试业务逻辑层的时候须要具体的数据,而后才能继续往下走.

好比如下方法,咱们获取数据库里的全部数据,然而经过一系列逻辑进行过滤,最终返回过滤后的结果.

public IEnumerable<MyDto> GetAllDtos()
        {
            var all = _dataBaseContext.GetAll().ToList();
            if (!all.Any()) return Enumerable.Empty<MyDto>();
            //一系列逻辑...
            var filteredDtos = all.Where(a => a.Age > 20);
            var orderDtos = filteredDtos.OrderBy(a => a.Name);
            return orderDtos;
        }

若是是默认行为(调用模拟的接口方法,引用对象返回null,集合返回空,简单对象返回默认值),则代码很快就返回了,if下面的业务逻辑测不到了.下面咱们看下如何配置接口方法的返回值

这里其实主要用到了 新建moq对象的setup方法,咱们能够在setup里设置方法,属性的值.

[Test]
        public void ShouldReturn_A_Collection_Of_Dtos()
        {
            var moq = new Mock<IDataBaseContext<MyDto>>();
            moq.Setup(a => a.GetAll()).Returns(new List<MyDto>
            {
                new MyDto{Name="baidu",Age=15},
                new MyDto{Name="sto",Age=32},
                new MyDto{Name="zto",Age=24},
                new MyDto{Name="yto",Age=12}
            });
            MyBll bll = new MyBll(moq.Object);
            var dtos = bll.GetAllDtos().ToList();
            dtos.Should().HaveCount(2);
            dtos.Select(a => a.Name).Should().BeInAscendingOrder();
        }

咱们看以上代码,咱们咱们让数据访问接口的代理对象返回一个MyDto类型集合,一共四个元素,由咱们的业务可知,咱们只要年龄大于20的元素,而且名字按正序排列.所以以上测试应该返回成功,实际上也是测试经过了.

带参数的方法设置

以上的GetAll是不带参数的,带参数的方法咱们能够显式的指定一个参数,咱们也可使用Moq框架提供的方法来模糊指定参数,好比咱们能够指定方法是任意字符,任意数字,任意范围的数字等.

咱们再看前面的一个方法

public MyDto GetADto(string id)
        {
            if (string.IsNullOrWhiteSpace(id)) return null;
            return _dataBaseContext.GetElementById(id);
        }

这个方法接收一个类型为字符串的id,只要字符串不是空字符串或者null时咱们都返回一个MyDto对象.

测试方法以下

[Test]
        public void ShouldReturn_A_Dto_If_QueryBy_Id_With_Valid_Parameter()
        {
            var moq = new Mock<IDataBaseContext<MyDto>>();
           moq.Setup(a => a.GetElementById(It.IsAny<string>())).Returns(new MyDto());
            MyBll bll = new MyBll(moq.Object);
            var dto = bll.GetADto("afakeid");
            dto.Should().NotBeNull();
        }

这里咱们使用到了Moq里的It.Is方法,这个方法接受一个Func<T,bool>类型的委托,咱们的条件是无论它是一个什么样的string,老是返回一个new MyDto();

[warning]注意这里配置的是Moq对象(即moq.Object)的方法返回值,而不是bll对象的方法的返回值,若是咱们传入的字符串是空字符串,则GetADto直接返回了null,数据访问对象就没机会执行了.

It里面还有不少静态方法,用于指定数字是不是否在某一范围,对象是不是列表中的对象,字符串是否知足正则等.语义都很是明确,你们能够本身研究一下.

指定参数的配置

以上使用到了It.IsAny方法.It里面还有一个Is方法,接受一个Func<T,bool>类型委托,用于指定对象为知足特定条件的对象,而不是任意对象.

Bll层新增如下方法

public bool IsVip(string id)
        {
            if (string.IsNullOrWhiteSpace(id)) return false;
            var dto = _dataBaseContext.GetElementById(id);
            if (dto?.Name?.Contains("sto")) return true;
            return false;
        }

咱们判断一个dto是不是vip,若是传入id为null返回false,若是不是则获取一个对象,若是对象的名字包含sto关键字则返回true

好比咱们知道id为9527的对象为sto,所以它是个vip,咱们的测试方法以下

[Test]
        public void ShouldReturn_True_If_Id_Is_9527()
        {
            var moq = new Mock<IDataBaseContext<MyDto>>();
            moq.Setup(a => a.GetElementById(It.Is<string>(t => t.Trim() == "9527"))).Returns(new MyDto { Name = "sto", Age = 24 });
            MyBll bll = new MyBll(moq.Object);
            bool isVip = bll.IsVip("9527");
            Assert.True(isVip);
        }

以上测试经过.

MOCk.Of

咱们以上仅配置了接口表明的一个方法,有时候须要配置多个,这样须要多个Setup,这时候咱们可使用Mock.Of,注意Mock.Of建立出来的是一个代理对象,而不是一个mock对象.

[Test]
        public void MockOf_Test()
        {
            var obj = Mock.Of<IDataBaseContext<MyDto>>(a =>a.GetAll()==new List<MyDto>(){new MyDto()}
                                                           &&a.GetElementById(It.IsAny<string>())==new MyDto()
                                                           &&a.GetElementsByName(It.IsAny<string>())==new MyDto[3]);
            var all = obj.GetAll();
            var one = obj.GetElementById("s");
            var some = obj.GetElementsByName("somename");
            Assert.Multiple(() =>
            {
                Assert.AreEqual(1, all.Count());
                Assert.NotNull(one);
                Assert.AreEqual(3, some.Count());
            });
        }

以上测试会经过.

注意以上的xxx==xxx并非比较两个对象,Mock利用它进行赋值

不少初接触单元测试的朋友看完以上代码后可能感受一脸懵,彻底不理解利用moq在dao层生成一些看似无心义的假数据有什么意义,其实你们要明白单元测试的目的是什么,单元测试是以代码块为基础(一般是一个方法),测试这一个单元逻辑的正确性,在dao层,咱们只关心这一层拿到数据后的处理逻辑.不少朋友可能知道ef能够搭建内存服务器来模拟真实数据库,这样也一样不依赖于外部的数据库.其实你们也能够这样作,也能够不这样而使用moq来模拟一个数据库链接上下文对象.由于在单元测试里,真实的数据是什么样的并非首要关心的问题,而是这个代码单元逻辑的正确性.若是是作集成测试,咱们则须要模拟一个真实环境,这个时候咱们就须要使用内存服务器甚至使用外部服务器.固然,若是要作压力测试,咱们还须要模拟产品运行时真实的物理环境,网络环境等条件(固然,不少时候直接在真实的运行环境进行测试了).总之咱们要搞清楚不一样的测试要解决什么样的问题,要达到什么样的目的,剩下的才是工具框架的使用.