Mock 框架 Moq 的使用
Mock 框架 Moq 的使用
Intro
Moq 是 .NET 中一个很流行的 Mock 框架,使用 Mock 框架我们可以只针对我们关注的代码进行测试,对于依赖项使用 Mock 对象配置预期的依赖服务的行为。
Moq 是基于 Castle
的动态代理来实现的,基于动态代理技术动态生成满足指定行为的类型
在一个项目里, 我们经常需要把某一部分程序独立出来以便我们可以对这部分进行测试. 这就要求我们不要考虑项目其余部分的复杂性, 我们只想关注需要被测试的那部分. 这里就需要用到模拟(Mock)技术.
因为, 请仔细看. 我们想要隔离测试的这部分代码对外部有一个或者多个依赖. 所以编写测试代码的时候, 我们需要提供这些依赖. 而针对隔离测试, 并不应该使用生产时用的依赖项, 所以我们使用模拟版本的依赖项, 这些模拟版依赖项只能用于测试时, 它们会使隔离更加容易.
绿色的是需要被测试的类,黄色是Mock的依赖项
——引用自杨旭大佬的博文
Prepare
首先我们需要先准备一下用于测试的类和接口,下面的示例都是基于下面定义的类和方法来做的
public interface IUserIdProvider
{
string GetUserId();
}
public class TestModel
{
public int Id { get; set; }
}
public interface IRepository
{
int Version { get; set; }
int GetCount();
Task<int> GetCountAsync();
TestModel GetById(int id);
List<TestModel> GetList();
TResult GetResult<TResult>(string sql);
int GetNum<T>();
bool Delete(int id);
}
public class TestService
{
private readonly IRepository _repository;
public TestService(IRepository repository)
{
_repository = repository;
}
public int Version
{
get => _repository.Version;
set => _repository.Version = value;
}
public List<TestModel> GetList() => _repository.GetList();
public TResult GetResult<TResult>(string sql) => _repository.GetResult<TResult>(sql);
public int GetResult(string sql) => _repository.GetResult<int>(sql);
public int GetNum<T>() => _repository.GetNum<T>();
public int GetCount() => _repository.GetCount();
public Task<int> GetCountAsync() => _repository.GetCountAsync();
public TestModel GetById(int id) => _repository.GetById(id);
public bool Delete(TestModel model) => _repository.Delete(model.Id);
}
我们要测试的类型就是类似 TestService
这样的,而 IRepositoy<TestModel>
和 IUserIdProvider
是属于外部依赖
Mock Method
Get Started
通常我们使用 Moq 最常用的可能就是 Mock 一个方法了,最简单的一个示例如下:
[Fact]
public void BasicTest()
{
var userIdProviderMock = new Mock<IUserIdProvider>();
userIdProviderMock.Setup(x => x.GetUserId()).Returns("mock");
Assert.Equal("mock", userIdProviderMock.Object.GetUserId());
}
Match Arguments
通常我们的方法很多是带有参数的,在使用 Moq 的时候我们可以通过设置参数匹配为不同的参数返回不同的结果,来看下面的这个例子:
[Fact]
public void MethodParameterMatch()
{
var repositoryMock = new Mock<IRepository>();
repositoryMock.Setup(x => x.Delete(It.IsAny<int>()))
.Returns(true);
repositoryMock.Setup(x => x.GetById(It.Is<int>(_ => _ > 0)))
.Returns((int id) => new TestModel()
{
Id = id
});
var service = new TestService(repositoryMock.Object);
var deleted = service.Delete(new TestModel());
Assert.True(deleted);
var result = service.GetById(1);
Assert.NotNull(result);
Assert.Equal(1, result.Id);
result = service.GetById(-1);
Assert.Null(result);
repositoryMock.Setup(x => x.GetById(It.Is<int>(_ => _ <= 0)))
.Returns(() => new TestModel()
{
Id = -1
});
result = service.GetById(0);
Assert.NotNull(result);
Assert.Equal(-1, result.Id);
}
通过 It.IsAny<T>
来表示匹配这个类型的所有值,通过 It.Is<T>(Expression<Func<bool>>)
来设置一个表达式来断言这个类型的值
通过上面的例子,我们可以看的出来,设置返回值的时候,可以直接设置一个固定的返回值,也可以设置一个委托来返回一个值,也可以根据方法的参数来动态配置返回结果
Async Method
现在很多地方都是在用异步方法,Moq 设置异步方法有三种方式,一起来看一下示例:
[Fact]
public async Task AsyncMethod()
{
var repositoryMock = new Mock<IRepository>();
// Task.FromResult
repositoryMock.Setup(x => x.GetCountAsync())
.Returns(Task.FromResult(10));
// ReturnAsync
repositoryMock.Setup(x => x.GetCountAsync())
.ReturnsAsync(10);
// Mock Result, start from 4.16
repositoryMock.Setup(x => x.GetCountAsync().Result)
.Returns(10);
var service = new TestService(repositoryMock.Object);
var result = await service.GetCountAsync();
Assert.True(result > 0);
}
还有一个方式也可以,但是不推荐,编译器也会给出一个警告,就是下面这样
repositoryMock.Setup(x => x.GetCountAsync()).Returns(async () => 10);
Generic Type
有些方法会是泛型方法,对于泛型方法,我们来看下面的示例:
[Fact]
public void GenericType()
{
var repositoryMock = new Mock<IRepository>();
var service = new TestService(repositoryMock.Object);
repositoryMock.Setup(x => x.GetResult<int>(It.IsAny<string>()))
.Returns(1);
Assert.Equal(1, service.GetResult(""));
repositoryMock.Setup(x => x.GetResult<string>(It.IsAny<string>()))
.Returns("test");
Assert.Equal("test", service.GetResult<string>(""));
}
[Fact]
public void GenericTypeMatch()
{
var repositoryMock = new Mock<IRepository>();
var service = new TestService(repositoryMock.Object);
repositoryMock.Setup(m => m.GetNum<It.IsAnyType>())
.Returns(-1);
repositoryMock.Setup(m => m.GetNum<It.IsSubtype<TestModel>>())
.Returns(0);
repositoryMock.Setup(m => m.GetNum<string>())
.Returns(1);
repositoryMock.Setup(m => m.GetNum<int>())
.Returns(2);
Assert.Equal(0, service.GetNum<TestModel>());
Assert.Equal(1, service.GetNum<string>());
Assert.Equal(2, service.GetNum<int>());
Assert.Equal(-1, service.GetNum<byte>());
}
如果要 Mock
指定类型的数据,可以直接指定泛型类型,如上面的第一个测试用例,如果要不同类型设置不同的结果一种是直接设置类型,如果要指定某个类型或者某个类型的子类,可以用 It.IsSubtype<T>
,如果要指定值类型可以用 It.IsValueType
,如果要匹配所有类型则可以用 It.IsAnyType
Callback
我们在设置 Mock 行为的时候可以设置 callback 来模拟方法执行时的逻辑,来看一下下面的示例:
[Fact]
public void Callback()
{
var deletedIds = new List<int>();
var repositoryMock = new Mock<IRepository>();
var service = new TestService(repositoryMock.Object);
repositoryMock.Setup(x => x.Delete(It.IsAny<int>()))
.Callback((int id) =>
{
deletedIds.Add(id);
})
.Returns(true);
for (var i = 0; i < 10; i++)
{
service.Delete(new TestModel() { Id = i });
}
Assert.Equal(10, deletedIds.Count);
for (var i = 0; i < 10; i++)
{
Assert.Equal(i, deletedIds[i]);
}
}
Verification
有时候我们会验证某个方法是否执行,并不需要关注是否方法的返回值,这时我们可以使用 Verification
验证某个方法是否被调用,示例如下:
[Fact]
public void Verification()
{
var repositoryMock = new Mock<IRepository>();
var service = new TestService(repositoryMock.Object);
service.Delete(new TestModel()
{
Id = 1
});
repositoryMock.Verify(x => x.Delete(1));
repositoryMock.Verify(x => x.Version, Times.Never());
Assert.Throws<MockException>(() => repositoryMock.Verify(x => x.Delete(2)));
}
如果方法没有被调用,就会引发一个 MockException
异常:
Verification
也可以指定方法触发的次数,比如:repositoryMock.Verify(x => x.Version, Times.Never);
,默认是 Times.AtLeastOnce
,可以指定具体次数 Times.Exactly(1)
或者指定一个范围 Times.Between(1,2, Range.Inclusive)
,Moq 也提供了一些比较方便的方法,比如Times.Never()
/Times.Once()
/Times.AtLeaseOnce()
/Times.AtMostOnce()
/Times.AtLease(2)
/Times.AtMost(2)
Mock Property
Moq 也可以 mock 属性,property 的本质是方法加一个字段,所以也可以用 Mock 方法的方式来 Mock 属性,只是使用 Mock 方法的方式进行 Mock 属性的话,后续修改属性值就不会引起属性值的变化了,如果修改属性,则要使用 SetupProperty
的方式来 Mock 属性,具体可以参考下面的这个示例:
[Fact]
public void Property()
{
var repositoryMock = new Mock<IRepository>();
var service = new TestService(repositoryMock.Object);
repositoryMock.Setup(x => x.Version).Returns(1);
Assert.Equal(1, service.Version);
service.Version = 2;
Assert.Equal(1, service.Version);
}
[Fact]
public void PropertyTracking()
{
var repositoryMock = new Mock<IRepository>();
var service = new TestService(repositoryMock.Object);
repositoryMock.SetupProperty(x => x.Version, 1);
Assert.Equal(1, service.Version);
service.Version = 2;
Assert.Equal(2, service.Version);
}
Sequence
我们可以通过 Sequence
来指定一个方法执行多次返回不同结果的效果,看一下示例就明白了:
[Fact]
public void Sequence()
{
var repositoryMock = new Mock<IRepository>();
var service = new TestService(repositoryMock.Object);
repositoryMock.SetupSequence(x => x.GetCount())
.Returns(1)
.Returns(2)
.Returns(3)
.Throws(new InvalidOperationException());
Assert.Equal(1, service.GetCount());
Assert.Equal(2, service.GetCount());
Assert.Equal(3, service.GetCount());
Assert.Throws<InvalidOperationException>(() => service.GetCount());
}
第一次调用返回值是1,第二次是2,第三次是3,第四次是抛了一个 InvalidOperationException
LINQ to Mocks
我们可以通过 Mock.Of
来实现类似 LINQ 的方式,创建一个 mock 对象实例,指定类型的实例,如果对象比较深,要 mock 的对象比较多使用这种方式可能会一定程度上简化自己的代码,来看使用示例:
[Fact]
public void MockLinq()
{
var services = Mock.Of<IServiceProvider>(sp =>
sp.GetService(typeof(IRepository)) == Mock.Of<IRepository>(r => r.Version == 1) &&
sp.GetService(typeof(IUserIdProvider)) == Mock.Of<IUserIdProvider>(a => a.GetUserId() == "test"));
Assert.Equal(1, services.ResolveService<IRepository>().Version);
Assert.Equal("test", services.ResolveService<IUserIdProvider>().GetUserId());
}
Mock Behavior
默认的 Mock Behavior 是 Loose
,默认没有设置预期行为的时候不会抛异常,会返回方法返回值类型的默认值或者空数组或者空枚举,
在声明 Mock
对象的时候可以指定 Behavior 为 Strict
,这样就是一个"真正"的 mock 对象,没有设置预期行为的时候就会抛出异常,示例如下:
[Fact]
public void MockBehaviorTest()
{
// Make mock behave like a "true Mock",
// raising exceptions for anything that doesn't have a corresponding expectation: in Moq slang a "Strict" mock;
// default behavior is "Loose" mock,
// which never throws and returns default values or empty arrays, enumerable, etc
var repositoryMock = new Mock<IRepository>();
var service = new TestService(repositoryMock.Object);
Assert.Equal(0, service.GetCount());
Assert.Null(service.GetList());
var arrayResult = repositoryMock.Object.GetArray();
Assert.NotNull(arrayResult);
Assert.Empty(arrayResult);
repositoryMock = new Mock<IRepository>(MockBehavior.Strict);
Assert.Throws<MockException>(() => new TestService(repositoryMock.Object).GetCount());
}
使用 Strict
模式不设置预期行为的时候就会报异常,异常信息类似下面这样:
More
Moq 还有一些别的用法,还支持事件的操作,还有 Protected 成员的 Mock,还有一些高级的用法,自定义 Default 行为等,感觉我们平时可能并不太常用,所以上面并没有加以介绍,有需要用的可以参考 Moq 的文档
上述测试代码可以在 Github 获取 https://github.com/WeihanLi/SamplesInPractice/blob/master/XunitSample/MoqTest.cs
References
- https://github.com/moq/moq4/wiki/Quickstart
- https://github.com/moq/moq4
- https://github.com/WeihanLi/SamplesInPractice/blob/master/XunitSample/MoqTest.cs
- https://www.cnblogs.com/tylerzhou/p/11410337.html
- https://www.cnblogs.com/cgzl/p/9304567.html
- https://www.cnblogs.com/haogj/archive/2011/07/22/2113496.html
Mock 框架 Moq 的使用的更多相关文章
- .NET Core之单元测试(三):Mock框架Moq的使用
编写一个API 新增一个接口 public interface IFoo { bool Ping(string ip); } 接口实现 public class Foo : IFoo { public ...
- MoQ(基于.net3.5,c#3.0的mock框架)简单介绍
我们在做单元测试的时候,常常困扰于数据的持久化问题,很多情况下我们不希望单元测试影响到数据库中的内容,而且受数据库的影响有时我们的单元测试的速度会很慢,所以我们往往希望将持久化部分隔离开,做单元测试的 ...
- Mock框架
MoQ(基于.net3.5,c#3.0的mock框架)简单介绍 - 你听海是不是在笑 - 博客园 http://www.cnblogs.com/nuaalfm/archive/2009/11/25/1 ...
- 教育单元测试mock框架优化之路(中)
转载:https://sq.163yun.com/blog/article/169564470918451200 三.间接依赖的bean的mock替换 对于前面提供的@Mock,@Spy+@Injec ...
- 教育单元测试mock框架优化之路(下)
转载:https://sq.163yun.com/blog/article/169563599967031296 四.循环依赖的解决 果然! 当我将@SpyBean应用到存在有循环依赖的Bean上时, ...
- 教育单元测试mock框架优化之路(上)
转载:https://sq.163yun.com/blog/article/169561874192850944 众所周知,mock对于单元测试,尤其是基于spring容器的单元测试,是非常重要的.它 ...
- 单元测试mock框架——jmockit实战
JMockit是google code上面的一个java单元测试mock项目,她很方便地让你对单元测试中的final类,静态方法,构造方法进行mock,功能强大.项目地址在:http://jmocki ...
- 推荐一款数据mock框架,无需任何依赖,贼牛逼
fox-mock 是基于Java Agent实现的自测,联调Mock利器.能解决你的这些问题: 开发过程中,依赖了下游多个接口,想跑个单测都必须得等下游把服务部署好 联调过程中,下游某个接口出问题,阻 ...
- MoQ(基于.net3.5,c#3.0的mock框架)简单介绍(转)
https://www.cnblogs.com/nuaalfm/archive/2009/11/25/1610755.html
随机推荐
- 002、Python中json字符串与字典转换
1.测试用例文件TestCase.xlsx 2.编写Python文件进行读取 #!/usr/bin/env python # -*- coding:utf-8 -*- import time impo ...
- HttpClient客户端网络编程——高可用、高并发
本文是HttpClient的学习博客,RestTemplate是基于HttpClient的封装,feign可基于HttpClient进行网络通信. 那么作为较底层的客户端网络编程框架,该怎么配置使其能 ...
- Tomcat基本原理
思考 :怎样让Tomcat具备Web服务的功能呢? 在服务端用HTTP来监听,协议不好写,不妨用Java封装好的Socket作为监听. class MyTomcat{ ServerSocket ser ...
- Java | 在 Java 中执行动态表达式语句: 前中后缀、Ognl、SpEL、Groovy、Jexl3
在一些规则集或者工作流项目中,经常会遇到动态解析表达式并执行得出结果的功能. 规则引擎是一种嵌入在应用程序中的组件,它可以将业务规则从业务代码中剥离出来,使用预先定义好的语义规范来实现这些剥离出来的业 ...
- 推荐一个vuepress模板,一键快速搭建文档站
介绍 vuepress-template是一个简单的VuePress案例模板,目的是让用户可以直接clone这个仓库,作为初始化一个VuePress网站启动项目,然后在这个项目的基础上新增自定义配置和 ...
- 【GitChat首秀:限时免费】互联网测试岗校招的那些事儿
2020 校园秋招即将结束,2021 校招春招即将开始. 作为一名扎根互联网近十年的资深测试开发,我刚经历过数十个测试岗位的校招笔试和面试选拔.在 2020 年秋招面试过程中,我深深地感受到" ...
- Bash on Ubuntu on Windows ( Windows Subsystem for Linux)
1 # Bash on ubuntu on Windows http://www.cnblogs.com/anonymous-ufo/p/6143480.html 1 1 如何启用Bash on u ...
- php 配置主机虚拟目录(使用虚拟域名访问 127.0.0.1) 一点也不好使?????
php 配置主机虚拟目录(使用虚拟域名访问 127.0.0.1)steps:1>打开目录 D:\xwamp\bin\apache\apache2.4.9\conf 修改文件 httpd ...
- Apple Watch Series 6 全天候视网膜显示屏和全天候高度计是什么鬼
Apple Watch Series 6 全天候视网膜显示屏和全天候高度计是什么鬼 Apple Watch Series 6 / Apple Watch Series 5 全天候视网膜显示屏 LTPO ...
- js binary search algorithm
js binary search algorithm js 二分查找算法 二分查找, 前置条件 存储在数组中 有序排列 理想条件: 数组是递增排列,数组中的元素互不相同; 重排 & 去重 顺序 ...