如何在 ASP.NET Core 测试中操纵时间?
2018-08-15 17:19
459 查看
有时候,我们会遇到一些跟系统当前时间相关的需求,例如:
- 只有开学季才允许录入学生信息
- 只有到了晚上或者周六才允许备份博客
- 注册满 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的替代品满足以下需求:
- 由于测试用例往往是多线程并行随机执行,所以替代品在线程间需要相互隔离
- 在集成测试中,ASP.NET Core 服务端代码与测试代码并不是运行在同一个线程中的,这时候,替代品需要能够在线程中共享
- 能够随时的设置当前时间
- 在生产环境中,必须与
DateTime.Now
功能一致 - 替代品的签名要与
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 Web API请求的响应时间
- 如何在ASP.NET Core Web API测试中使用Postman
- 如何一秒钟从头构建一个 ASP.NET Core 中间件
- 如何设置ASP.NET页面的运行超时时间
- 如何设置 ASP.NET 页缓存的过期时间值
- 如何在ASP.NET Core中实现一个基础的身份认证
- 如何为asp.net core添加protobuf支持详解
- ASP.NET Core应用的错误处理[4]:StatusCodePagesMiddleware中间件如何针对响应码呈现错误页面
- asp.net core生成路由连接,请求参数如何获取和模型绑定
- ASP.NET Core 中文文档 第四章 MVC(4.5)测试控制器逻辑
- 如何进行ASP.NET MVC 的测试
- asp.net core1.x/asp.net core2.0中如何加载多个配置文件
- ASP.NET Core 如何上传单个文件
- ASP.NET Core如何添加统一模型验证处理机制详解
- 如何在ASP.NET Core中实现CORS跨域
- ASP.NET MVC单元测试时如何对含有ModelState.IsValid的Action进行测试
- [引]ASP.NET 移动网页 与 如何:使用仿真程序和浏览器在部署移动 Web 应用程序之前对其进行测试
- PHP的strtotime()函数转换的时间如何转换成ASP.NET识别的,或者反过来
- Asp.net core与golang web简单对比测试
- 如何在ASP.NET Core应用程序运行Vue并且部署在IIS上详解