Click here to Skip to main content
15,884,472 members
Articles / Programming Languages / C#

Making Time-dependent Features Testable

Rate me:
Please Sign up or sign in to vote.
5.00/5 (1 vote)
17 Oct 2020MIT6 min read 4.4K   44   5   2
Description of the technique that makes the time-dependent code unit-testable
As professional developers, we are all striving to provide 100% automated test coverage for all code we write. However, one particular class of features is quite difficult to test - those that depend on current time, in one way or another. For example, an expiring token - how we test that it actually expires in 20 minutes? In this article, I share a simple technique that helps with this challenge, that I have been personally using for quite some time. As an extra bonus, I will also share the 'simplest' stopwatch utility methods.

The Time Testing Problem and a Solution

Let's start with a simple case that often comes up in practice. Let's say you have a simple object - a Token - that has a lifetime, so its state is dependent on the current time. The token is valid immediately after it is created, but it expires after 20 minutes:

C#
public class Token {
  DateTime _created; 
  
  public Token() {
    _created = DateTime.UtcNow; 
  }
  
  public bool IsValid() {
    return DateTime.UtcNow < _created.AddMinutes(20);
  }
}

Quite simple code, but real-life tokens might be a lot more complex. Now the question is - how to unit test it? Straight solution:

C#
var token = new Token();
Assert.IsTrue(token.IsValid());
Thread.Sleep(21 * 60 * 1000); // wait 21 minutes
Assert.IsFalse(token.IsValid());

This is obviously beyond ridiculous. Nobody would accept a 'test' that hangs for 20 minutes. We can move the constant 20 (for minutes) to some settings object (which we should do anyway), and then try to set much lower value for testing, but this does not eliminate the problem completely. And in real life applications, with many settings, many time-related constants, it might become hard to do, and it also introduces risk of significantly altering testing environment (what we test vs what we run in production).

In manual testing, for an entire application, QA engineers sometimes use workarounds. Let's say this token is an access token produced at login. The tester logs-in, checks that she can access the app, then quickly changes the current time on the computer (moves it forward 30 minutes); and tries the app again. The token should 'expire' and the app should not be available. This is a race against the time service on the machine, which regularly sets the correct time using network service. Anyway, this is tricky, and does not work for unit testing anyway.

So how to make it work? The idea is to create a wrapper for DateTime.UtcNow that allows 'shifting' the current time for an application. And I already see some raised eyebrows - what? You suggest to modify the app to make your test run? Well, not exactly. We do not change the application logic. We just make this logic testable. I think there is nothing wrong with this approach.

So let's introduce a new AppTime static class that will serve us as a new API to access the current time:

C#
public static class AppTime {
  private static TimeSpan _offset = TimeSpan.Zero;

  public static DateTime UtcNow {
    get {
      var now = DateTime.UtcNow;
      return (_offset == TimeSpan.Zero) ? now : now.Add(_offset);
    }
  }

  public static void SetOffset(TimeSpan offset) {
    _offset = offset;
  }

  public static void ClearOffset() {
    _offset = TimeSpan.Zero;
  }
}

The AppTime provides access to current UTC time, but you can introduce a permanent shift in returned values. This is effectively a time machine, which allows shifting 'application time' at will.

The Token class code now changes to:

C#
public class Token {
  DateTime _created; 
  
  public Token() {
    _created = AppTime.UtcNow; 
  }
  
  public bool IsValid() {
    return AppTime.UtcNow < _created.AddMinutes(20);
  }
}

and testing becomes trivial:

C#
var token = new Token();
Assert.IsTrue(token.IsValid());
// shift app time forward
AppTime.SetOffset(TimeSpan.FromMinutes(21));
Assert.IsFalse(token.IsValid());
AppTime.ClearOffset();

I want to note one important and subtle point here. What happens if the second assertion fails, from a bug in Token.IsValid() method? The Assert will throw exception, and the last ClearOffset() will not be executed. Seems like not a big deal, but if you are running a battery of tests, some other time-dependent tests executing AFTER this failed test will fail too - because they start with already shifted time. You will see a whole bunch of failed tests, with no clue that the original cause was this single faulty method.

To avoid this, always place AppTime.ClearOffset() into Test-cleanup method of the test class; this method executes after every test, and will guarantee tests isolation from each other.

The general strategy is the following - everywhere in your application and libraries you use AppTime.UtcNow, not DateTime.UtcNow. This will provide you with a time machine for the entire application. The AppTime class, full listing with comments is provided with the download zip for this article.

Using Apptime in Manual Testing

It may seem at first that usefulness of this technique is limited to code-based, automated testing only. Manual testing, through UI clicking - the tester does not have an access to AppTime class on the server, so how can she use it? There is actually a way.

Let's go back to login token example. The QA person tries to test login and login-expiration feature in a Web ASP.NET Core app. She logs-in and then tries to check that the access token expires in 20 minutes. You, the developer, can provide a secret URL, enabled only in test environment, with a controller method behind it that allows to set the time offset to a value specified in URL parameter in some form (as well another option to clear the offset). So the QA engineer logs-in, opens another tab in browser, hits the time-offset URL shifting time forward by 30 minutes. Then goes back to the app tab and tries to click something - she should be rejected, login token expired. Test passed!

Bonus - Time-Measurement Methods in Apptime Class

The AppTime class provides a few convenience methods that simplify measuring the precise execution time of a code span. Here is a typical code fragment that does execute-and-measure-time sequence:

C#
// measuring operation time
var sw = new Stopwatch();
sw.Start();

DoStuff();

sw.Stop();
var opTime = sw.Elapsed;

The AppTime class provides utility methods that simplify this a bit:

C#
// measuring operation time
var start = AppTime.GetTimestamp();
DoStuff();
var opTime = AppTime.GetDuration(start);

Turns out the Stopwatch class has a few static methods, and you can measure the time interval using just these methods, with sub-millisecond precision, without creating an instance of the Stopwatch.

Of course, not a big deal of improvement - 2 lines instead of 4 in original version. But what I find attractive in the second version is that it seems less mentally distracting in the context of the method. When we read the code, we want to stay focused on the main function, what method is doing; time-measuring aspect is important, but it is essentially a side aspect, monitoring code. And the less distracting this sideline code is - the better. In the original code, having a 'new' operator immediately grabs attention and you have to spend a split-second mental effort to realize that it is for time measurement, not mainstream functionality. With AppTime method, it seems immediately clear what it is, so it is quickly discarded. This is, of course, a matter of personal perception and even taste. Anyway, use it if you like it.

Using the Code

The attached code is a very simple console app, with full listing of the AppTime class.

Points of Interest

We used UtcNow in all our examples (UTC time). The DateTime class also provides Now property (local time), and the AppTime class has it too (shifted local time). However, I would strongly discourage you from using these properties, ever. Most of our code works on the servers, and the code should NOT depend on server's location/time. Servers in the cloud are always on UTC time, no matter physical location. Use UTC time, so that even when you run/test on your development laptop, your code works the same as on production server. To sum it up: stay away from local time, unless you absolutely have to use it; the cases mostly should be limited to showing the date-time values in UI.

History

  • 17th October, 2020: Initial version

License

This article, along with any associated source code and files, is licensed under The MIT License


Written By
Software Developer (Senior) Microsoft
United States United States
25 years of professional experience. .NET/c#, databases, security.
Currently Senior Security Engineer, Cloud Security, Microsoft

Comments and Discussions

 
QuestionWhy not just Pin
Sacha Barber17-Oct-20 21:07
Sacha Barber17-Oct-20 21:07 
AnswerRe: Why not just Pin
Roman Ivantsov18-Oct-20 9:58
professionalRoman Ivantsov18-Oct-20 9:58 
well of course, you can come up with many variations on the theme, and the ones that fit your style.
I only have a little problem with the word "just" in your comment, which supposedly introduces a simpler version. I would say the AppTime static class is the simplest, and requires a minimum explanation effort to a newcomer - "Use AppTime.UtcNow instead of DateTime.UtcNow." Not much changes in code base, easy to change the existing codebase.
Another aspect - possible perf impact; DateTime.UtcNow is very fast, I read somewhere it's around 10ns; so the replacement should not drastically change this number; time service and DI container lookup might be a bit more expensive

thank you

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Praise Praise    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.