单元测试的核心就是:只测试眼前的逻辑。这就要求所有的依赖项都要使用仿类来代替,也就是所谓的 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. Spring IOC之Classpath扫描和管理的组件

    在前面的大部分例子我们使用XML去指明配置数据去定义在Spring容器中的每一个BeanDefinition.上一节我们展示了如何在 代码层注解的方式来提供大量的配置信息.即使在这些例子中,但是,基础 ...

  2. MUI初始化滚动区域

    mui(".mui-scroll-wrapper").scroll().refresh(); 如果不是MUI对象,需要转一下才可 mui($("#areaDiv" ...

  3. javascript 学习总结(三)Boolean对象

    Boolean对象 /* 创建 Boolean 对象的语法: new Boolean(value); //构造函数 Boolean(value); //转换函数 参数 value 由布尔对象存放的值或 ...

  4. Scala很难!

    Scala很难! 本文是从 Yes, Virginia, Scala is hard 这篇文章翻译而来. 首先要说的是,我是一个Scala粉丝,我作为一个Scala语言的倡导者差不多有5年历史了.我写 ...

  5. Play framework 2.0

    Play framework 2.0北京时间3月14日消息,根据Play framework官方网站消息,目前Play framework 2.0正式版已经发布.新版本的Play framework进 ...

  6. Hibernate在自由状态和持久的状态转变

    在Hibernate在.一PO术后可能长时间,session过时关闭.此时PO它一直是游离状态的对象,在这种状态下,以被转换成持久战,有几种方法如下: 1.session.saveOrUpdate(o ...

  7. BitMap画图

    package com.example.examples_05_07; import android.content.Context; import android.graphics.Bitmap; ...

  8. idea执行go

    因为经常在不同的地方调代码,每次都调整环境很麻烦,于是在犯懒的时候发现了更直接简便的办法,关于idea集成go环境的,不需要按部就班的部署. 首先下代码,比如https://github.com/sa ...

  9. 【IOS开发】SimPholders的使用

    推荐一个Xocde开发工具 “SimPholders”,能够快速访问到你的模拟器文件夹,最重要的是完全免费! 官方地址

  10. Bootstrap3.0学习第三轮(栅格系统案例)

    Bootstrap3.0学习第三轮(栅格系统案例) 前言 在前面的一篇文章当中http://www.cnblogs.com/aehyok/p/3400499.html主要学习了栅格系统的基本原理,以及 ...