Sometimes, we encounter some requirements related to the current time of the system, such as:
- Only the opening season is allowed to enter student information
- It's only in the evenings or Saturday to allow backup blogs.
- Users who have registered for 3 days are allowed to do something
- A user is banned from speaking within 24 hours
It is clear that the code to implement these functions is more or less a DateTime.Now
static attribute, but to use unit testing or integration testing to verify the above requirements, often need to adopt some curve saving method or even skip these tests directly, because in. Net, it is DateTime.Now
often difficult to Mock. That's when I'm going to boast a Angular test tool that provides a perfect Mock-up of Date
objects, so it's easy to manipulate the "current time" when writing test code.
After a few online searches, I found that the. Net FrameWork used to have such tools, not just mocks DateTime.Now
, but many other mscorlib.dll
methods and properties that could be mock. Such tools are broadly divided into three categories based on how they work, the first of which is to provide a mscorlib.dll
way to generate false, and then add the generated fake DLL to the test project, the second class is to create a separate at run time AppDomain
, and then in this AppDomain
Temporarily generates an in-memory pseudo-assembly when the assembly is loaded, and another is to modify the reference address of the target function/property directly at run time. Of these three solutions, I am personally more inclined to the second-more flexible and will not change the existing process. However, the results of these searches are basically .Net Framework
development-oriented, support for. Net Core and free of charge tools that I haven't found yet. Now I'm looking at smocks this project and trying to move him to. NET core, as a result of the lack of necessary APIs in Netstandard and the progress of Microsoft's development, they estimate that it will be up to. NET Core 3.0 to fill these APIs, this project can wait, But the project I have on hand can not afford, no way, only a botched replacement DateTime.Now
to achieve similar functions.
With what to replace
DateTime.Now
?
A qualified DateTime.Now
alternative meets the following requirements:
- Because test cases tend to be multithreaded parallel random execution, the alternatives need to be isolated from each other in the thread
- In integration testing, the ASP and the test code are not running in the same thread, when the alternatives need to be able to be shared in the threads
- Ability to set the current time at any time
- In a production environment, it must be
DateTime.Now
consistent with functionality
- The signature of substitutes should be
DateTime.Now
consistent with
On the basis of this answer on the Web, I have myself transformed a class that is available in the ASP. NET Core Integration Test SystemClock
:
<SUMMARY>///provides access to system time and allowing it to BES set to a fixed <see cref= "DateTime"/> value.///</summary>///<remarks>///This class is thread safe.///</remarks>public static class System clock{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 =&Gt GetTime (). ToUniversalTime (); <summary>//sets a fixed (deterministic) time for the current thread to return by <see cref= "DateTime"/& gt;. </summary> public static void Set (DateTime time) {if (time. Kind! = datetimekind.local) time = time. ToLocalTime (); Clocksmap[clockid.value] = () = time; }///<summary>//Initialize clock with a ID, so-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.N ow "/> </summary> public static void Reset () {Clocksmap[clockid.value] = Default; }}
In the product code, you need to replace all of them manually DateTime.Now
SystemClock.Now
.
In the test code, a manual call is required SystemClock.Init(clockId)
to initialize, which stores the incoming clockId
storage as a static variable in the current thread, and sets a separate delegate for the Id to return DateTime
. When used SystemClock.Now
, it looks for the corresponding delegate in the current thread ClockId
and returns the execution result. Thus, as long as multiple threads SystemClock.Init
are in the same clockId
call, we can share or set the return result in either of these threads, SystemClock.Now
and the different threads, if clockid, are not affected by SystemClock.Now
each other.
For example, let's say that there is one, in order to allow us to modify the execution result in the thread TestStartup.cs
of execution of the test case code Controller
, we SystemClock.Now
first need to set the Configure
method:
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(); });}
Since the thread that we requested might not be the same each time, I added the initialization code to the first middleware SystemClock
. In the test case, we can manipulate the time:
public async void SomeTest(){ var now = new DateTime(2022,1,1); SystemClock.Set(now); // 注册用户 // Assert: 用户还不可以发言 var threeDaysAfter = now.AddDays(3); SystemClock.Set(threeDaysAfter); // Assert: 用户可以发言了
An idea
Because manual replacement DateTime.Now
has a large change to existing code, the above is just a simple temporary workaround. But to solve this problem is not very difficult, you can try to dotnet build
After the generated all the DLL through the tool to process, in the results of the compilation of the replacement of DateTime.Now
, But recently there is not so much time, so remember here first (Dig pit reservation).