单元测试的核心就是:只测试眼前的逻辑。这就要求所有的依赖项都要使用仿类来代替,也就是所谓的 Mock Object。在测试 ProfileRepositoryAccountController 的时候,我遇到了需要对 UserManagerSignInManager 进行 Mock 的需求。因为这两个组件相互依赖,还依赖别的组件,我折腾了好一阵才搞定这个问题。具体的方法分两种:直接使用 Moq 进行 Mock 和使用 InMemory Database 进行 Mock。下面分别来说明一下。

一、 使用 InMemory Database 进行 Mock

ProfileRepository 的测试中,我使用了 InMemory 这个方案。因为之前对单元测试的一些误解(使用 PHPUnit 而遗留下来的想法),我最直接想到的就是在数据库中添加数据,然后让各个组件去直接读数据库。当然,为了让测试能够飞速运行,我需要使用一个在内存里运行的数据库。但严格来说,这样就不算是单元测试了,而有一些集成测试的味道。只是使用内存数据库,速度上并没有那么慢,所以权且当成是一种扩展版的单元测试吧。这里有两个内存数据库可以选:一个是 SQLite 的 :memory: 模式,这个是一个接近完整的数据库,只是在外键的约束上可能还有点问题;另一个是 EF Core 的 InMenory 数据库。这个只是一个内存里保存数据的容器,其实并不是一个数据库,没有 SQLite 那样的数据一致性检查。这里,我使用的是 InMemory Database,这样可以让这个测试更“单元”一点:

public ProfileRepositoryTests()
{
var services = new ServiceCollection();
services.AddEntityFramework()
.AddEntityFrameworkInMemoryDatabase()
.AddDbContext<ApplicationDbContext>(options => {
options.UseInMemoryDatabase();
}); services.AddIdentity<ApplicationUser, IdentityRole>()
.AddEntityFrameworkStores<ApplicationDbContext>(); // Taken from https://github.com/aspnet/MusicStore/blob/dev/test/MusicStore.Test/ManageControllerTest.cs (and modified)
// IHttpContextAccessor is required for SignInManager, and UserManager
var context = new DefaultHttpContext();
context.Features.Set<IHttpAuthenticationFeature>(
new HttpAuthenticationFeature());
services.AddSingleton<IHttpContextAccessor>(h =>
new HttpContextAccessor { HttpContext = context }); var serviceProvider = services.BuildServiceProvider();
_dbContext = serviceProvider.GetRequiredService<ApplicationDbContext>();
_userManager = serviceProvider.GetRequiredService<UserManager<ApplicationUser>>(); Task.Run(async () => {
await _userManager.CreateAsync(new ApplicationUser {
UserName = "test1" }, "11aaAA_");
await _userManager.CreateAsync(new ApplicationUser {
UserName = "test2" }, "11aaAA_"); var user = await _userManager.FindByNameAsync("test2"); var profile = new Profile()
{
AccountID = user.Id,
Avatar = "avatar-file"
};
_dbContext.Add(profile);
_dbContext.SaveChanges(); user.ProfileID = profile.Id;
_dbContext.Update(user);
_dbContext.SaveChanges();
}).Wait();
}

可以看到,其实就是把 Startup 里的一些内容复制过来而已。这里 _userManager_dbContext 都是做为 TestClass 的成员而存在的。可以使用 Property,也可以使用成员变量。这样做的意义主要是一些测试可能需要再增加一些数据,歌者直接去 Assert 数据库里有对应的数据。当然,如果为了更灵活的测试,这里的添加数据的问题也可以提出去,做了一个单独的函数,然后在 Arrange 阶段来调用。怎么安排测试的结构,取决于测试的复杂度和个人的风格,没有太多的标准。

二、直接使用 Moq 来 Mock

在 Mock AccountController 里的 UserManager 时,我发现了另一个解决方案,相比上面的方案,这个更加直接一些:

public static Mock<SignInManager<TUser>>
MockSignInManager<TUser>(Mock<UserManager<TUser>> manager)
where TUser : class
{
var context = new Mock<HttpContext>();
// var manager = MockUserManager<TUser>();
return new Mock<SignInManager<TUser>>(manager.Object,
new HttpContextAccessor { HttpContext = context.Object },
new Mock<IUserClaimsPrincipalFactory<TUser>>().Object,
null, null)
{ CallBase = true };
} public static Mock<UserManager<TUser>> MockUserManager<TUser>()
where TUser : class
{
IList<IUserValidator<TUser>> UserValidators =
new List<IUserValidator<TUser>>();
IList<IPasswordValidator<TUser>> PasswordValidators =
new List<IPasswordValidator<TUser>>(); var store = new Mock<IUserStore<TUser>>();
UserValidators.Add(new UserValidator<TUser>());
PasswordValidators.Add(new PasswordValidator<TUser>());
var mgr = new Mock<UserManager<TUser>>(store.Object, null, null,
UserValidators, PasswordValidators, null, null, null, null);
return mgr;
}

使用这两个函数,就可以直接创建 UserManagerSignInManager 的 Mock 了。不过,在使用 SignInManager 模拟登录的时候还要注意:

_mockSignInManager.Setup(m =>
m.PasswordSignInAsync(It.IsAny<ApplicationUser>(),
It.IsAny<string>(),
It.IsAny<bool>(),
It.IsAny<bool>()))
.Returns(Task.FromResult(SignInResult.Success));

也就是说,创建“登录成功”,不能直接 new 一个 SignInResult,因为不能修改 SignInResult 的状态,而是要使用它已经写好的带状态的结果。

这两种方式各有用处。比如 InMemory Database 的方案,不但可以对 UserManagerSignInManager 的结果进行控制,还提供了一个可以写入和检查的数据库。而直接 Mock 的方案,则干扰更少,更专注于逻辑。我个人感觉,在对 Repository 的测试中,使用 InMemory Database 可能更合适一点,然后在其它地方,因为 Repository 隔离了数据访问,所以可以直接对 Repository 进行 Mock,这时候就可以使用直接 Mock 的方案。

三、Logger 的 Mock

在测试 AccountController 的时候,还需要对 ILoggerILoggerFactory 进行 Mock,这当然也不是什么难事:

_mockLogger.Setup(m => m.Log(It.IsAny<LogLevel>(),
It.IsAny<EventId>(),
It.IsAny<FormattedLogValues>(),
It.IsAny<Exception>(),
It.IsAny<Func<object, Exception, string>>()));
_mockLoggerFactory.Setup(m =>
m.CreateLogger(It.IsAny<string>())).Returns(_mockLogger.Object);

也就是,得 Mock 两个东西。这当然是因为 Controller 里都是依赖于 ILoggerFactory ,然后再使用 factory 创建 ILogger

四、UrlHelper 的 Mock

最后一个坑是 UrlHelper。通常一个 Controller 都会有 RedirectTo 一个 Action 或者一个 URL 的需求,那就不可避免要用到 UrlHelper。而 Controller 需要单独进行 Mock:

var mockUrlHelper = new Mock<IUrlHelper>();
mockUrlHelper.Setup(m => m.IsLocalUrl(It.IsAny<string>())).Returns(true);
controller.Url = mockUrlHelper.Object;

下面参考资料里的代码要复杂的多,应该是因为 ASP.NET Core 的版本问题造成的。我这个“简单的版本”,是针对 1.1.0 版本的。如果以后有变化,可能会在别的地方再说明吧。

完整的代码请到下面两个 repo 中的一个去看:

GitHub: http://github.com/holmescn/CoreCRM

Codint.NET: https://coding.net/u/holmescn/p/CoreCRM/git

参考链接:

直接 Mock 的代码是请看这里

使用 InMemory Database 的请看这里

UrlHelper 的原始想法来自这里

CoreCRM 开发实录 —— 单元测试之 Mock UserManager 和 SignInManager的更多相关文章

  1. python笔记24-unittest单元测试之mock.patch

    前言 上一篇python笔记23-unittest单元测试之mock对mock已经有初步的认识, 本篇继续介绍mock里面另一种实现方式,patch装饰器的使用,patch() 作为函数装饰器,为您创 ...

  2. 单元测试之Mock

    为什么需要Mock. 真实对象具有不确定的行为.所以会产生不可预测的结果. 真实对象很难被创建. 真实对象的某些行为很难被触发(如网络错误). 真实对象令程序的运行速度很慢. 真实对象有(或者是)用户 ...

  3. python文档2-unittest单元测试之mock.patch

    介绍mock里面另一种实现方式,patch装饰器的使用,patch() 作为函数装饰器,为您创建模拟并将其传递到装饰函数 patch简介 1.unittest.mock.patch(target,ne ...

  4. CoreCRM 开发实录 —— 单元测试、测试驱动开发和在线服务

    测试不是问题,问题是怎么测试. ## 单元测试 我认为单元测试已经是无可争议的最佳开发实践之一.但是很多人并不同意这个观点.他们的说法无非是:写测试需要花很多时间,需求又经常变动,一但变动,一大片测试 ...

  5. python笔记23-unittest单元测试之mock

    什么是mock unittest.mock是一个用于在Python中进行单元测试的库,Mock翻译过来就是模拟的意思,顾名思义这个库的主要功能是模拟一些东西. 它的主要功能是使用mock对象替代掉指定 ...

  6. python文档1-unittest单元测试之mock

    什么是mock unittest.mock是一个用于在Python中进行单元测试的库,Mock翻译过来就是模拟的意思,顾名思义这个库的主要功能是模拟一些东西.它的主要功能是使用mock对象替代掉指定的 ...

  7. CoreCRM 开发实录——Travis-CI 实现 .NET Core 程度在 macOS 上的构建和测试 [无水干货]

    上一篇文章我提到:为了使用"国货",我把 Linux 上的构建和测试委托给了 DaoCloud,而 Travis-CI 不能放着不用啊.还好,这货支持 macOS 系统.所以就把 ...

  8. CoreCRM 开发实录——开始之新项目的技术选择

    2016年11月,接受了一个工作,是对"悟空CRM"进行一些修补.这是一个不错的 CRM,开源,并提供一个 SaaS 的服务.正好微软的 .NET Core 和 ASP.NET C ...

  9. CoreCRM 开发实录 —— 前后端分离的重构

    虽然2月初就回来了,可 CoreCRM 一直到5月才开始恢复开发,期间是各种生活中的意外和不方便. 1. 为什么要重构 首先是一件很值得高兴的事情:CoreCRM 有了第一位 contributor! ...

随机推荐

  1. 转载:善待Redis中的数据

    Redis是我们数据的保管者,我们可以随时存随时取,大的小的,重要的不重要的,它都毫无怨言的帮我们保存着,甚至有些时候,我们变得很懒,存东西进去的时候顺便还贴张纸:"过了一个星期就帮我扔了吧 ...

  2. Canvas入门(3):图像处理和渲染文本

    资源:http://www.ido321.com/997.html 一.图像处理(非特别说明,全部结果均来自最新版Google) 在HTML 5中,不仅能够使用Canvas API绘制图形,也能够用于 ...

  3. swift 笔记 (二十) —— 泛型

    泛型 泛型是为了解决在针对不同数据类型.而做了同一种功能的操作导致的每一个类型我们都要写一份代码的问题. 有了泛型,我们能够仅仅写一份逻辑代码,而适应于不同的数据类型. func swapInt(in ...

  4. HPQC HP Quality Center windows 服务

    HPQC HP Quality Center windows 服务已经启动的话,就不用运行run.bat 两个是一样的效果.

  5. Protocol Buffer和JSON性能比较

      JSON PB 数据结构支持 简单结构 较复杂结构 数据格式 文本 二进制 数据大小 一般 小,json大小的1/3左右 解析效率 一般 快,是json解析速度的3-10倍 可读性 好,自描述的 ...

  6. CanVas类(绘图类)

    知识点: Canvas():创建一个空的画布,可以使用setBitmap()方法来设置具体的画布 Canvas(Bitmap bitmap):以bitmap对象创建一个画布,则将内容都绘制在Bitma ...

  7. 关于WCF的一些知识点

    首先,WCF和WebService一些区别1,WCF支持多中通信协议,http/https tcp/udp/msmq.命名管道,对等网,消息可达性,事物流等.2,WCF可以与ASP.NET集成,共享同 ...

  8. C#方法同步 [MethodImpl(MethodImplOptions.Synchronized)]

    using System; using System.Collections.Generic; using System.Linq; using System.Text; using System.T ...

  9. Quartz.net 定时计划使用

    新建解决方案和工程Quartz.net 使用Power Shell 命令 Install-Package Quartz 导入Quartz.net程序集 新建一个计划TestJob using Syst ...

  10. 昨天面试新浪 java试题

      昨天去了新浪网面试,感觉新浪真的挺不错的,工作环境那叫一个好啊.对于一般屌丝的话进到这种公司就可以呆一辈子了.做了面试之后感觉不管大公司还是小公司都还是注重基础和你平时工作的积累的.所以不能抱怨现 ...