单元测试的核心就是:只测试眼前的逻辑。这就要求所有的依赖项都要使用仿类来代替,也就是所谓的 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. 间支付系统,DataGridView

    我们通常看到很多的学习使用控制数据库和接口连接--DataGridView,在我们的房间,当我们敲开使用第一遍阶段似该控件--MSHFlexGrid,随着学习的深入,发现我们用到的平台越来越人性化了, ...

  2. Java 类的成员初始化顺序

    做个简单笔录,就当是重温下基础知识. 1.先看代码: package com.test; public class Test { public static void main(String[] ar ...

  3. vim的复制粘贴小结

    vim帮助文档里与粘贴板有关的内容如下: vim有12个粘贴板,分别是0.1.2.….9.a.“.+:用:reg命令可以查看各个粘贴板里的内容.在vim中简单用y只是复制到“(双引号)粘贴板里,同样用 ...

  4. [译]在运行时内存中的Java对象是怎么样的

    (文章翻译自What do Java objects look like in memory during run-time?) 我们知道函数在内存中是作为一个活动记录栈来实现的.而且我们知道Java ...

  5. ftoa浮法成字符串

    #include <stdio.h> bool ftos(float num,char *s,int n) {     int temp; float t=num; int pn=0; b ...

  6. QC邮件转发工具Mail Direct安装配置手册

    QC邮件转发工具Mail Direct安装配置手册 2010-06-11 10:00:56| 分类: 软件测试 | 标签: |举报 |字号大中小 订阅 QC邮件转发工具安装配置手册 由于公司没有独立的 ...

  7. .net图片裁剪抠图之性能优化

    //.net图片裁剪抠图:1.将不坐标点存入GraphicsPath中:GraphicsPath gPath = new GraphicsPath();2. 通常我们判断一个坐标点是否在闭合区间内通采 ...

  8. Java(5/6和8)中interface和Abstract Class

    这篇文章主要是自己在使用java的过程中对自己一些之前常困惑的问题的一些总结. 正如题目所言,这篇博客主要是讨论java中的接口与抽象类的区别,有自己的使用心得,以及自己平时在使用的过程中遇到的问题及 ...

  9. MVC 在控制器中获取某个视图动态的HTML代码

    ASP.NET MVC 在控制器中获取某个视图动态的HTML代码   如果我们需要动态的用AJAX从服务器端获取HTML代码,拼接字符串是一种不好的方式,所以我们将HTML代码写在cshtml文件中, ...

  10. 企业架构与建模之使用ArchiMate进行分析

    企业架构与建模之使用ArchiMate进行分析(全系列完) 4. 使用ArchiMate进行分析 正如前面所说的那样,一个企业整体效率的提升有时并不是通过某一个领域内的优化就能达到的,而且这种忽视全局 ...