Mr. Boy
Mr. Boy

Reputation: 63710

Is it possible to speed up time for unit testing?

Is there any reasonable way you can speed up (or fake) the passage of time for unit testing? If for instance, you want to test signaling in threads, or that some code correctly handles day/month/year date changes in long-running code?

I know you can check things like day/year change in separate tests but moving towards an integration test it can be nice to be able to run a solid week's time passing without waiting a week... if you have something happening hourly then being able to drive that in some fast-forward mechanism.

Upvotes: 4

Views: 2626

Answers (2)

komsky
komsky

Reputation: 1588

I was looking for a simple solution to this problem, and this is the first page that pops up in the search engine, but does not provide any examples, so I've implemented this one myself. Here you go.

Start with a simple interface

public interface ICoordinatedDateTimeService
{
    DateTime UtcNow { get; }
    public int SpeedMultiplier { get; }
}

Expose it via some static class, eg.:

public static class DateTimeService
{
    static ICoordinatedDateTimeService _dateTimeService;
    static bool _initialized;
    public static ICoordinatedDateTimeService CoordinatedDateTimeService 
    {
        get
        {
            if (_dateTimeService == null)
            {
                throw new Exception("DateTimeService must be initialized before first use");
            }
            return _dateTimeService;
        }
        set 
        {
            //we are blocking multiple initializations/assignments
            //to avoid issues when the backtesting is running
            if (_initialized)
            {
                throw new Exception("DateTimeService can be initialized only once");
            }
            else
            {
                _dateTimeService = value;
                _initialized = true;
                
            }                 
        } 
    }
}

Now implement the interface in 2 classes, a real-time service, that will return actual time - this is for regular runs, and a sped-up service, that will return sped-up time.

Regular time service:

public class HostDateTimeService : ICoordinatedDateTimeService
{
    public DateTime UtcNow => DateTime.UtcNow;

    public int SpeedMultiplier => 1;
}

Sped-up time service:

public class BacktestingDateTimeService : ICoordinatedDateTimeService
{
    public int SpeedMultiplier { get { return _speedMultiplier; } }
    private int _speedMultiplier;
    private DateTime _startingPoint;
    private DateTime _referencePoint;

    public BacktestingDateTimeService(int speedMultiplier, DateTime startingPoint)
    {
        _speedMultiplier = speedMultiplier;
        _startingPoint = startingPoint;
        _referencePoint = DateTime.UtcNow;
    }

    public DateTime UtcNow { get { return CalculateUtcNow(); } }

    private DateTime CalculateUtcNow()
    {
        var numberOfSecondsPassedSinceStart = DateTime.UtcNow - _referencePoint;
        var speedUpNow = _startingPoint.AddSeconds(numberOfSecondsPassedSinceStart.TotalSeconds * _speedMultiplier);
        if (speedUpNow > DateTime.UtcNow) return DateTime.UtcNow; //can't move past the current date
        return speedUpNow;
    }
}

Now you have two time services you can use accordingly, one for real life scenarios and other for testing. In your project, replace all DateTime.UtcNow references with DateTimeService.CoordinatedDateTimeService.UtcNow The DateTimeService.CoordinatedDateTimeService service must be initialized with either your regular time service or the sped-up time service when you start your program:

 DateTimeService.CoordinatedDateTimeService = new BacktestingDateTimeService();

nUnit test I've used to see if it works:

    [Test]
    public async Task OneMinuteIsOneMonthTest()
    {
        DateTime startingPoint = DateTime.UtcNow.AddYears(-1);
        _sut = new BacktestingDateTimeService(60 * 24 * 30, startingPoint);
        await Task.Delay(60000); //wait for one second
        var expected = DateTime.UtcNow.AddYears(-1).AddMonths(1);
        var actual = _sut.UtcNow; ;
        Assert.AreEqual(expected.Year, actual.Year);
        Assert.AreEqual(expected.Month, actual.Month); 
        
        await Task.Delay(60000); //wait for one second
        var newExpected = DateTime.UtcNow.AddYears(-1).AddMonths(2);
        Assert.AreEqual(newExpected.Year, _sut.UtcNow.Year);
        Assert.AreEqual(newExpected.Month, _sut.UtcNow.Month);
    }

Upvotes: 1

Arnon Axelrod
Arnon Axelrod

Reputation: 1672

As @Nkosi and @mike mentioned in the comments, you should abstract the date/time APIs behind interfaces, so the test can control what the CUT sees as the current date/time. The same goes for Thread.Sleep. This is pretty straight-forward in unit-tests, especially if you're doing TDD.

For integration tests or system tests, this can be more challenging. In most cases this can be resolved in a similar manner, but the CUT should use some sort of Dependency Injection mechanism.

I once was an automation TL for a pretty big project and we had this need to. Fortunately, because most of the code was already covered by unit-tests, most of the code that had to refer to Date/Time was already abstracted using an interface. In addition, the system was designed with extensibility in mind, and used a DI mechanism. So it was possible to register a DLL containing implementations of the necessary interfaces that can simulate the time shifts.

Of course it was necessary to have some communication mechanism between the test and that DLL, because the DLL is loaded to the application's process, which is different than the test's.

One thing we realized pretty soon is that you should never return back in time, even at the test's cleanup, because the application should never face such case in the real world. You should only reset the time to the current time when you revert the entire environment to a known state.

One more word of warning: If the application interacts with external systems that rely on date/time (even the database!), then this probably won't work for you, unless you abstract away the external systems entirely, which in some cases loose all the benefits of the integration tests.

Upvotes: 1

Related Questions