咨询热线:15259079798 联系人:劳春强 地址:贵州省桐梓县河滨北路
如何在 ASP.NET Core 测试中操纵时间?
来源:918搏天堂备用网址 发布时间:2019-12-03 点击量:327
有时候,我们会遇到一些跟系统当前时间相关的需求,例如:
只有开学季才允许录入学生信息只有到了晚上或者周六才允许备份博客注册满 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
,但是最近并没有这么多时间,所以先在这里记着⛏(挖坑预定)。
相关产品
-
思想上积极参加学习,坚持售楼原则,维护公司的形象及声誉,做到热情接待,温情待看,亲情售楼,友情交流,主动向比自己有经验的同事学习交流,采盘观摩,在短短的时间内比较熟悉的掌握销售流程。
-
按照Gartner数据,中兴年内实现6000万台智能手机的目标应该问题不大,关键是其中国区能否扭转销售颓势,以及美国市场和其他新兴市场能否继续业绩上扬。
-
会议最后决议,不会成立相关附例,要求所有招牌只能使用英语,取而代之,是采用“清除堆积措施”(Decluttering Initiative)规范商铺厨窗、招贴中文广告或海报所占面积的大小;至于如何才合乎规格,则暂时未有定案,有待进一步研究。
-
中银慧投是中国银行“坚持科技引领、创新驱动,转型求实、变革图强,加快建设新时代全球一流银行”发展理念在金融科技方面的先行实践,是建设数字化、智能化银行的重要战略举措,是AI+金融的大胆尝试。
-
有相关人士告诉北京晨报记者:“今年国青组队后,曾有过几次海外拉练,当时皇马都派出了自己的球探考察来自各国的年轻球员,在之后的熊猫杯比赛里,皇马也派球探跟踪考察。当时皇马球探就看好了林良铭,并且进行了相当长时间的观察。”对于林良铭,不少人都给出“这孩子一心想去国外踢球”的介绍,所以至今网上也还流传着“恒大不满没能买到林良铭,许家印埋怨属下”的消息。
-
与那些生活方式最不健康(5个健康习惯全不具备)的成年人相比,保持最健康生活方式(5个健康习惯全都具备)的人群,死于心血管疾病的风险要低82%,死于癌症的风险要低65%,在跟踪调查期间死亡的风险总体要低74%。
-
江苏省昆山市委常委、花桥经济开发区管委会主任杨军表示,昆山海峡两岸电子商务综合服务平台是大陆目前唯一能和多个城市通关系统对接的平台,未来还将实现重要跨境口岸的全部互通。平台能为两岸电商提供全方位、一站式服务,大大缩短商品在途时间和物流成本。
-
相比于人工核验票证的窗口前排起长队,北京西站自助验票口的队伍较短,记者于中午11点多,从自助验票口进站,用时12分钟完成了验票、安检的进站流程。
热点资讯
- 冯小刚开创的贺岁档,在21年后消弭于无形2019-07-01
- 洛杉矶银河队官宣瑞典前锋伊布下赛季留队2019-07-01
- 2018大沥镇黄岐龙母诞举行2019-07-01
- Windows10达成里程碑全球市场份额终超50%2019-11-26
- 8.8智慧生活日大数据出炉:1000万人获免单,广州用户最多2019-10-29
- 《我不是药神》豆瓣9.0高分开局,电影提档至7月5日上映2019-11-24
- 《古墓丽影:暗影》游戏总监:我们根本不怕PS4《蜘蛛侠》2019-11-17
- 揭秘区块链的核心技术之「哈希与加密算法」2019-11-05