0

I have a method running in an infinite loop in a task from the ctor. The message does the equivalent to sending a ping, and upon a certain number of failed pings, prevents other commands from being able to be sent. The method is started from the ctor with Task.Factory.StartNew(()=>InitPingBackgroundTask(pingCts.token));

public async Task InitPingBackgroundTask (CancellationToken token)
{
   while(!token.IsCancellationRequested)
   {
      try 
      {
          Ping();
          // handle logic
          _pingsFailed = 0;
          _canSend = true;
      }
      catch(TimeoutException)
      {
          // handle logic
          if(++_pingsFailed >= settings.MaxPingLossNoConnection)
             _canSend = false;
      }
      finally
      {
          await Task.Delay(settings.PingInterval);
      }
   }
}

I have a second method DoCmd like so:

public void DoCmd()
{
   if(_canSend)
   {
      // handle logic
   }
}

In my test, using Moq, I can set the settings to have PingInterval=TimeSpan.FromSeconds(1) and MaxPingLossNoConnection = 2 for the sake of my test.

After setting up the sut and everything else, I want to call DoCmd() and verify that it is not actually sent. (I have mocked its dependencies and can verify that the logic in its method was never called)

One way to achieve this is to add a sleep or delay from within the test, before calling sut.DoCmd(), but this makes the test take longer. Is there a way to simulate time passing (like tick(desiredTime) in angular, or something similar)? To simulate the passing of time without having to actually wait the time?

Any leads would be appreciated :-)

EDIT Added the test code:

public void NotAllowSendIfTooManyPingsFailed()
{
   var settings = new Settings()
   {
      PingInterval=TimeSpan.FromSeconds(1),
      MaxPingLossNoConnection = 0 // ended up changing to 0 to fail immidiately
   }
   // set up code, that Ping messages should fail

   Thread.Sleep(100); // necessary to Ping has a chance to fail before method is called
   sut.DoCmd();

   // assert logic, verifying that DoCmd did not go through
}

Ideally, I would like to be able to play with MaxPingLossNoConnection and set it to different numbers to test circumstances. That would also require adding sleeps to let the time pass. In this test, I would want to remove the Sleep altogether, as well as in other similar tests where sleep would be longer

PMO1948
  • 2,210
  • 11
  • 34
  • You can create a new dependency that takes care of the waiting part, so you can set your mock to not wait at all, while the real implementation waits for `settings.PingInterval`. – insane_developer Mar 01 '21 at 16:15
  • How would you do that? – PMO1948 Mar 01 '21 at 16:18
  • I'm assuming you're familiar with dependency injection, so it's just a class implementing an interface with a method that executes `await Task.Delay(settings.PingInterval);`. Now you can mock that class and substitute the behavior of the method. This is just one way of doing it, and it seemed natural to me. – insane_developer Mar 01 '21 at 17:02
  • Isn't it a bit backwards to change something like that in the code, just to support tests? It's added more DI to multiple classes that have a delay, just for the sake of ease of testing – PMO1948 Mar 02 '21 at 07:35
  • I think you should consider to use some sort of locking around `_canSend` because it is a shared resource between multiple threads. I would also encourage you use prefer `Task.Run` over `Task.Factory.StartNew`. – Peter Csala Mar 02 '21 at 07:43
  • Could you please share with us the code of the test case? It is not a problem if it is not working or there are some TODOs in it. It could help us to better understand what you really want to achieve. – Peter Csala Mar 02 '21 at 07:45
  • @PeterCsala is the lock necessary, if there is only one thread that can change it? The others just read it, but there is a single test that changes it – PMO1948 Mar 02 '21 at 07:52
  • 1
    @PMO1948 This can cause race condition. Please check these sites to get more insight: [1](https://en.wikipedia.org/wiki/Readers%E2%80%93writer_lock), [2](https://stackoverflow.com/questions/27860685/how-to-make-a-multiple-read-single-write-lock-from-more-basic-synchronization-pr), [3](https://eli.thegreenplace.net/2019/implementing-reader-writer-locks/) – Peter Csala Mar 02 '21 at 08:07
  • Back to your test. You can think of your `finally { await Task.Delay ...` as a dependency. If you introduce a wrapper around it then you can mock it in your test. Please check this [related SO topic](https://stackoverflow.com/questions/40537996/mocking-task-delay) – Peter Csala Mar 02 '21 at 08:11
  • 1
    @PMO1948 that's the whole mantra of testable code. There are books written about it. It's also very common in Domain Driven Design to abstract things like `DateTime.Now` and such. I'm not here to convince you to adopt best testability practices but just answering your question. – insane_developer Mar 02 '21 at 14:19

1 Answers1

0

In order to deal with time concerns im my tests, I found a little Clock class a while ago and replaced all the calls to DateTime.Now in my systems for Clock.Now. I tweaked it a little bit to have the option to set the time and freeze it or to keep it running during the tests.

Clock.cs

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;

namespace ComumLib
{
    public class Clock : IDisposable
    {
        private static DateTime? _startTime;
        private static DateTime? _nowForTest;

        public static DateTime Now
        {
            get
            { 
                if (_nowForTest == null)
                {
                    return DateTime.Now;
                }
                else
                {
                    //freeze time
                    if (_startTime == null)
                    {
                        return _nowForTest.GetValueOrDefault();
                    }
                    //keep running
                    else
                    {
                        TimeSpan elapsedTime = DateTime.Now.Subtract(_startTime.GetValueOrDefault());
                        return _nowForTest.GetValueOrDefault().Add(elapsedTime);
                    }
                }
            }
        }

        public static IDisposable NowIs(DateTime dateTime, bool keepTimeRunning = false)
        {
            _nowForTest = dateTime;
            if (keepTimeRunning)
            {
                _startTime = DateTime.Now;
            }
            return new Clock();
        }

        public static IDisposable ResetNowIs()
        {
            _startTime = null;
            _nowForTest = null;
            return new Clock();
        }

        public void Dispose()
        {
            _startTime = null;
            _nowForTest = null;
        }
    }
}

In my tests, I either call

DateTime dt = new DateTime(2022,01,01,22,12,44,2);

Clock.NowIs(dt,true); //time keeps running during the test

or

Clock.NowIs(dt); //time remains the same during the test
rod0302
  • 121
  • 1
  • 5