有时候,我们会遇到一些跟系统当前时间相关的需求,例如:

  • 只有开学季才允许录入学生信息
  • 只有到了晚上或者周六才允许备份博客
  • 注册满 3 天的用户才允许进行一些操作
  • 某用户在 24 小时内被禁止发言

很显然,要实现这些功能的代码多多少少要用到 DateTime.Now 这个静态属性,然而要使用单元测试或者集成测试对上述需求进行验证,往往需要采用一些曲线救国的方法甚至是直接跳过这些测试,这是因为在 .Net 中,DateTime.Now 通常难以被 Mock 。这时候我就要夸一夸 Angular 的测试工具了,较完美的提供了 Date 对象的 Mock 方法,所以在编写测试代码的时候可以很容易的操纵 “当前时间”。

在网上一番查阅过后,我发现 .Net FrameWork 中曾经是有这样的工具的,不仅仅是 Mock DateTime.Now,其他的很多来自于 mscorlib.dll 的方法、属性也可以被 Mock。这类工具根据工作原理大致分为三类,第一类是提供了一个生成假 mscorlib.dll 的方法,然后再把生成出来的假的 dll 添加到测试项目中,第二类则是在运行时创建一个独立的 AppDomain,然后在这个 AppDomain 中加载程序集的时候临时生成一个内存中的假程序集替换进去,还有一种则是直接在运行时修改目标函数/属性的引用地址。这三种解决方案中,我个人更倾向于第二种 —— 更加灵活,而且不会改变现有流程。不过,这些搜索到的结果基本上都是面向 .Net Framework 开发的,能支持 .Net Core 而且不收费的工具,我现在还没找到。现在我在关注的是 Smocks 这个项目,也尝试过把他迁移到 .Net Core 上,结果因为 netstandard 中缺少必要 API 而告终,看微软的开发进度,他们估计要到 .Net Core 3.0 才会补上这些 API,这个项目能等,但我手头上的项目等不起啊,没办法,只能先拙劣的替换 DateTime.Now 来实现类似的功能了。

用什么来代替 DateTime.Now

一个合格的 DateTime.Now 的替代品满足以下需求:

  1. 由于测试用例往往是多线程并行随机执行,所以替代品在线程间需要相互隔离
  2. 在集成测试中,ASP.NET Core 服务端代码与测试代码并不是运行在同一个线程中的,这时候,替代品需要能够在线程中共享
  3. 能够随时的设置当前时间
  4. 在生产环境中,必须与 DateTime.Now 功能一致
  5. 替代品的签名要与 DateTime.Now 一致

在爆栈网上的 这个答案的基础上,我自己改造了一个在 ASP.NET Core 集成测试中可用的 SystemClock 类:

/// <summary>
/// Provides access to system time while allowing it to be set to a fixed <see cref="DateTime"/> value.
/// </summary>
/// <remarks>
/// This class is thread safe.
/// </remarks>
public static class SystemClock
{
private static readonly Func<DateTime> Default = () => DateTime.Now; public static ThreadLocal<string> ClockId = new ThreadLocal<string>(() => "prod"); public static Dictionary<string, Func<DateTime>> ClocksMap = new Dictionary<string, Func<DateTime>>()
{
["prod"] = Default
};
private static DateTime GetTime()
{
var fn = ClocksMap[ClockId.Value] ?? Default;
return fn();
} /// <inheritdoc cref="DateTime.Today"/>
public static DateTime Today => GetTime().Date; /// <inheritdoc cref="DateTime.Now"/>
public static DateTime Now => GetTime(); /// <inheritdoc cref="DateTime.UtcNow"/>
public static DateTime UtcNow => GetTime().ToUniversalTime(); /// <summary>
/// Sets a fixed (deterministic) time for the current thread to return by <see cref="DateTime"/>.
/// </summary>
public static void Set(DateTime time)
{
if (time.Kind != DateTimeKind.Local)
time = time.ToLocalTime(); ClocksMap[ClockId.Value] = () => time;
} /// <summary>
/// Initialize clock with an id, so that you can share the clock across threads.
/// </summary>
/// <param name="clockId"></param>
public static void Init(string clockId)
{
ClockId.Value = clockId;
if (ClocksMap.ContainsKey(clockId) == false)
{
ClocksMap[clockId] = Default;
}
} /// <summary>
/// Resets <see cref="SystemClock"/> to return the current <see cref="DateTime.Now"/>.
/// </summary>
public static void Reset()
{
ClocksMap[ClockId.Value] = Default;
} }

在产品代码中,需要手动的把所有的 DateTime.Now 替换成 SystemClock.Now

在测试代码中,需要先手动调用 SystemClock.Init(clockId) 来进行初始化,它会把传入的 clockId 存储为一个当前线程中的一个静态变量,同时为这个 Id 设置一个单独的返回 DateTime 的委托。在使用 SystemClock.Now 的时候,它会寻找当前线程中 ClockId 对应的委托并返回执行结果。这样,只要多个线程中的 SystemClock.Init 是通过同样的 clockId 调用的,我们就可以在这些线程的任意一个中共享或者设置 SystemClock.Now 的返回结果,而不同的线程中,如果 ClockId,那么他们的 SystemClock.Now 相互不受影响。

举个例子,假设有一个 TestStartup.cs ,为了能够让我们在测试用例代码执行的线程中修改 Controller 执行线程中 SystemClock.Now 的执行结果,首先需要设置一下 Configure 方法:

public override void Configure(IApplicationBuilder app, IHostingEnvironment env, ILoggerFactory loggerFactory)
{
// TestStartup.Configure 会在测试线程中调用
var clockId = Guid.NewGuid().ToString();
SystemClock.Init(clockId);
app.Use(async (context, next) =>
{
// 中间件的执行线程与测试线程不同但与 Controller、Service 的执行线程相同
SystemClock.Init(clockId);
await next();
});
}

由于每次处理我们请求的线程可能并不是同一个,所以我就在第一个中间件中添加了初始化 SystemClock 的代码。在测试用例中,我们就可以操纵时间了:

public async void SomeTest()
{
var now = new DateTime(2022,1,1);
SystemClock.Set(now);
// 注册用户
// Assert: 用户还不可以发言
var threeDaysAfter = now.AddDays(3);
SystemClock.Set(threeDaysAfter);
// Assert: 用户可以发言了

一个想法

由于手动替换 DateTime.Now 对现有代码改动很大,所以上面提出的只是一个简单的临时应对方案。但要解决这个问题其实也不是很难,可以尝试在 dotnet build 之后把生成出来的所有 dll 通过工具处理一遍,在编译的结果中替换 DateTime.Now,但是最近并没有这么多时间,所以先在这里记着⛏(挖坑预定)。

如何在 ASP.NET Core 测试中操纵时间?的更多相关文章

  1. 如何在ASP.NET Core应用中实现与第三方IoC/DI框架的整合?

    我们知道整个ASP.NET Core建立在以ServiceCollection/ServiceProvider为核心的DI框架上,它甚至提供了扩展点使我们可以与第三方DI框架进行整合.对此比较了解的读 ...

  2. 在 ASP.NET Core 项目中使用 MediatR 实现中介者模式

    一.前言  最近有在看 DDD 的相关资料以及微软的 eShopOnContainers 这个项目中基于 DDD 的架构设计,在 Ordering 这个示例服务中,可以看到各层之间的代码调用与我们之前 ...

  3. ASP.NET Core SignalR中的流式传输

    什么是流式传输? 流式传输是这一种以稳定持续流的形式传输数据的技术. 流式传输的使用场景 有些场景中,服务器返回的数据量较大,等待时间较长,客户端不得不等待服务器返回所有数据后,再进行相应的操作.这时 ...

  4. 在 ASP.NET Core 项目中使用 AutoMapper 进行实体映射

    一.前言 在实际项目开发过程中,我们使用到的各种 ORM 组件都可以很便捷的将我们获取到的数据绑定到对应的 List<T> 集合中,因为我们最终想要在页面上展示的数据与数据库实体类之间可能 ...

  5. 采用最简单的方式在ASP.NET Core应用中实现认证、登录和注销

    在安全领域,认证和授权是两个重要的主题.认证是安全体系的第一道屏障,是守护整个应用或者服务的第一道大门.当访问者请求进入的时候,认证体系通过验证对方的提供凭证确定其真实身份.认证体系只有在证实了访问者 ...

  6. 如何在ASP.NET Core中使用Azure Service Bus Queue

    原文:USING AZURE SERVICE BUS QUEUES WITH ASP.NET CORE SERVICES 作者:damienbod 译文:如何在ASP.NET Core中使用Azure ...

  7. 如何在ASP.NET Core自定义中间件中读取Request.Body和Response.Body的内容?

    原文:如何在ASP.NET Core自定义中间件中读取Request.Body和Response.Body的内容? 文章名称: 如何在ASP.NET Core自定义中间件读取Request.Body和 ...

  8. 如何在ASP.NET Core中实现CORS跨域

    注:下载本文的完整代码示例请访问 > How to enable CORS(Cross-origin resource sharing) in ASP.NET Core 如何在ASP.NET C ...

  9. 如何在ASP.NET Core中实现一个基础的身份认证

    注:本文提到的代码示例下载地址> How to achieve a basic authorization in ASP.NET Core 如何在ASP.NET Core中实现一个基础的身份认证 ...

随机推荐

  1. lwip-动态内存管理

    动态内存管理涉及两类重要函数,内存分配函数,内存释放函数,如C语言中的malloc和free. 内存分配的本质是:在事先准好一大块内存堆(可以理解为一个很大的数组)中分配合适的空间,然后将该空间起始地 ...

  2. Shell脚本学习-数组

    跟着RUNOOB网站的教程学习的笔记 Shell数组 数组中可以存放多个值,Bash Shell只支持一维数组(不支持多维数组),初始化时不需要定义数组大小(与PHP类似). 与大部分编程语言类似,数 ...

  3. MVC+EF CODE FIRST的使用

    1创建标准MVC项目 2通过NuGet安装EF 3在Models文件夹中编写实体类 4创建EFDB上下文类 5在webconfig中创建连接字符串,其中name=EFDB上下文类名 6通过管理控制台执 ...

  4. maya2017卸载/安装失败/如何彻底卸载清除干净maya2017注册表和文件的方法

    maya2017提示安装未完成,某些产品无法安装该怎样解决呢?一些朋友在win7或者win10系统下安装maya2017失败提示maya2017安装未完成,某些产品无法安装,也有时候想重新安装maya ...

  5. 分布式系统中 Unique ID 的生成方法

    http://darktea.github.io/notes/2013/12/08/Unique-ID Snowflake 生成的 unique ID 的组成 (由高位到低位): 41 bits: T ...

  6. IDEA中读取 resource目录下文件

    1. 资源文件 2. 加载文件 public void test() { try { System.out.println("begin test"); String filepa ...

  7. ASP.NET Web API实现微信公众平台开发(三)自定义菜单

    承接之前的流程,在完成服务器绑定和获取access_token之后,本文主要讲述如何实现微信自定义菜单. 官方示例效果 开始之前 .自定义菜单最多包括3个一级菜单,每个一级菜单最多包含5个二级菜单. ...

  8. centos安装守护进程工具supervisor

    安装命令 yum install supervisor 启动守护进程 supervisord -c /etc/supervisord.conf 切换至/etc/supervisord.d目录下 写一个 ...

  9. Spark SQL 性能优化再进一步:CBO 基于代价的优化

    摘要: 本文将介绍 CBO,它充分考虑了数据本身的特点(如大小.分布)以及操作算子的特点(中间结果集的分布及大小)及代价,从而更好的选择执行代价最小的物理执行计划,即 SparkPlan. Spark ...

  10. linux中sed命令的使用

    sed命令是linux或者shell编程中常用的筛选.替换命令,如果能熟练使用sed则对经常使用的人来说在工作上是非常有帮助的 下面把sed主要的用法列出来(有错误的地方大家可以指正): p命令只打印 ...