When using System.Timers.Timer in your .NET C# application, you might face problems with abstracting it and being able to cover your modules with Unit Tests.
In this article, we would be discussing the Best Practices on how to conquer these challenges, and by the end, you would be able to achieve 100% coverage of your modules.
This is how we are going to approach our solution:
Come up with a very simple example to work on.
Start with the simple bad solution.
Keep trying to enhance it till we reach the final format.
Summarizing the lessons learned through our journey.
In our example, we will be building a simple Console Application which would do only one simple thing: use a System.Timers.Timer
to write to the console the date and time every second.
In the end, you should end up with this:
As you can see, it is simple in terms of requirements, nothing fancy.
Some best practices would be ignored/dropped in order to drive the main focus to the other best practices targeted in this article.
In this article, we would focus on covering the module using System.Timers.Timer with unit tests. However, the rest of the solution would not be covered with unit tests. If you would like to know more about this, you can check the article
There are some third-party libraries that could be used to achieve nearly similar results. However, whenever possible, I would rather follow a native simple design than depend on a whole big third-party library.
In this solution, we would directly use System.Timers.Timer without providing a layer of abstraction.
The structure of the solution should look like this:
It is a UsingTimer solution with only one Console TimerApp project.
I intentionally invested some time and effort in abstracting System.Console
into IConsole
to prove that this would not solve our problem with the Timer.
namespace TimerApp.Abstractions
{
public interface IConsole
{
void WriteLine(object? value);
}
}
We would only need to use System.Console.WriteLine
in our example; that’s why this is the only abstracted method.
namespace TimerApp.Abstractions
{
public interface IPublisher
{
void StartPublishing();
void StopPublishing();
}
}
We have only two methods on the IPublisher
interface: StartPublishing
and StopPublishing
.
Now, for the implementations:
using TimerApp.Abstractions;
namespace TimerApp.Implementations
{
public class Console : IConsole
{
public void WriteLine(object? value)
{
System.Console.WriteLine(value);
}
}
}
Console
is just a thin wrapper for System.Console
.
using System.Timers;
using TimerApp.Abstractions;
namespace TimerApp.Implementations
{
public class Publisher : IPublisher
{
private readonly Timer m_Timer;
private readonly IConsole m_Console;
public Publisher(IConsole console)
{
m_Timer = new Timer();
m_Timer.Enabled = true;
m_Timer.Interval = 1000;
m_Timer.Elapsed += Handler;
m_Console = console;
}
public void StartPublishing()
{
m_Timer.Start();
}
public void StopPublishing()
{
m_Timer.Stop();
}
private void Handler(object sender, ElapsedEventArgs args)
{
m_Console.WriteLine(args.SignalTime);
}
}
}
Publisher
is a simple implementation of IPublisher
. It is using a System.Timers.Timer
and just configuring it.
It has the IConsole
defined as a dependency. This is not a best practice from my point of view. If you want to understand what I mean, you can check the article
However, only for the sake of simplicity, we would just inject it as a dependency in the constructor.
We are also setting the Timer interval to 1000 Milliseconds (1 Second), and setting the handler to write the Timer SignalTime
to the Console.
using TimerApp.Abstractions;
using TimerApp.Implementations;
namespace TimerApp
{
public class Program
{
static void Main(string[] args)
{
IPublisher publisher = new Publisher(new Implementations.Console());
publisher.StartPublishing();
System.Console.ReadLine();
publisher.StopPublishing();
}
}
}
Here, in the Program
class, we are not doing much. We are just creating an instance of the Publisher
class and starting publishing.
Running this should end up with something like this:
Now, the question is, if you are going to write a unit test for the Publisher
class, what can you do?
Unfortunately, the answer would be: not too much.
First, you are not injecting the Timer itself as a dependency. This means that you are hiding the dependency inside the Publisher
class. Therefore, we can’t mock or stub the Timer.
Second, let’s say that we modified the code so that the Timer is now injected in the constructor; still, the question would be, how to write a unit test and replace the Timer with a mock or stub?
I hear someone shouting, “let’s wrap the Timer into an abstraction and inject it instead of the Timer.”
Yes, that’s right, however, it is not that simple. There are some tricks that I am going to explain in the next section.
This is the time for a good solution. Let’s see what we can do about it.
The structure of the solution should look like this:
It is the same UsingTimer solution with a new Console BetterTimerApp project.
IConsole
, IPublisher
, and Console
would be the same.
using System;
namespace BetterTimerApp.Abstractions
{
public delegate void TimerIntervalElapsedEventHandler(object sender, DateTime dateTime);
public interface ITimer : IDisposable
{
event TimerIntervalElapsedEventHandler TimerIntervalElapsed;
bool Enabled { get; set; }
double Interval { get; set; }
void Start();
void Stop();
}
}
What we can notice here:
We defined the new delegate TimerIntervalElapsedEventHandler
. This delegate represents the event to be raised by our ITimer
.
You might argue that we don’t need this new delegate as we already have the native ElapsedEventHandler
which is already used by System.Timers.Timer
.
Yes, this is true. However, you would notice that the ElapsedEventHandler
event is providing ElapsedEventArgs
as the event arguments. This ElapsedEventArgs
has a private constructor, and you would not be able to create your own instance. Additionally, the SignalTime
property defined in the ElapsedEventArgs
class is read-only. Therefore, you would not be able to override it in a child class.
There is a change request ticket opened for Microsoft to update this class, but up till the moment of writing this article, no changes were applied.
Also, note that ITimer
extends the IDisposable
.
using System;
using BetterTimerApp.Abstractions;
namespace BetterTimerApp.Implementations
{
public class Publisher : IPublisher
{
private readonly ITimer m_Timer;
private readonly IConsole m_Console;
public Publisher(ITimer timer, IConsole console)
{
m_Timer = timer;
m_Timer.Enabled = true;
m_Timer.Interval = 1000;
m_Timer.TimerIntervalElapsed += Handler;
m_Console = console;
}
public void StartPublishing()
{
m_Timer.Start();
}
public void StopPublishing()
{
m_Timer.Stop();
}
private void Handler(object sender, DateTime dateTime)
{
m_Console.WriteLine(dateTime);
}
}
}
It is almost the same as the old Publisher
except for small changes. Now, we have the ITimer
defined as a dependency that is injected through the constructor. The rest of the code would be the same.
using System;
using System.Collections.Generic;
using System.Linq;
using System.Timers;
using BetterTimerApp.Abstractions;
namespace BetterTimerApp.Implementations
{
public class Timer : ITimer
{
private Dictionary<TimerIntervalElapsedEventHandler, List<ElapsedEventHandler>> m_Handlers = new();
private bool m_IsDisposed;
private System.Timers.Timer m_Timer;
public Timer()
{
m_Timer = new System.Timers.Timer();
}
public event TimerIntervalElapsedEventHandler TimerIntervalElapsed
{
add
{
var internalHandler =
(ElapsedEventHandler)((sender, args) => { value.Invoke(sender, args.SignalTime); });
if (!m_Handlers.ContainsKey(value))
{
m_Handlers.Add(value, new List<ElapsedEventHandler>());
}
m_Handlers[value].Add(internalHandler);
m_Timer.Elapsed += internalHandler;
}
remove
{
m_Timer.Elapsed -= m_Handlers[value].Last();
m_Handlers[value].RemoveAt(m_Handlers[value].Count - 1);
if (!m_Handlers[value].Any())
{
m_Handlers.Remove(value);
}
}
}
public bool Enabled
{
get => m_Timer.Enabled;
set => m_Timer.Enabled = value;
}
public double Interval
{
get => m_Timer.Interval;
set => m_Timer.Interval = value;
}
public void Start()
{
m_Timer.Start();
}
public void Stop()
{
m_Timer.Stop();
}
public void Dispose()
{
Dispose(true);
GC.SuppressFinalize(this);
}
protected virtual void Dispose(bool disposing)
{
if (m_IsDisposed) return;
if (disposing && m_Handlers.Any())
{
foreach (var internalHandlers in m_Handlers.Values)
{
if (internalHandlers?.Any() ?? false)
{
internalHandlers.ForEach(handler => m_Timer.Elapsed -= handler);
}
}
m_Timer.Dispose();
m_Timer = null;
m_Handlers.Clear();
m_Handlers = null;
}
m_IsDisposed = true;
}
~Timer()
{
Dispose(false);
}
}
}
This is where almost all the magic happens.
What we can notice here:
Internally, we are using System.Timers.Timer
.
We applied the IDisposable design pattern. That’s why you can see the private bool m_IsDisposed
, public void Dispose()
, protected virtual void Dispose(bool disposing)
, and ~Timer()
.
In the constructor, we are initializing a new instance of System.Timers.Timer
. We would refer to this as the Internal Timer in the rest of the steps.
For public bool Enabled
, public double Interval
, public void Start()
, and public void Stop()
, we are just delegating the implementation to the Internal Timer.
For public event TimerIntervalElapsedEventHandler TimerIntervalElapsed
, this is the most important part; so let’s analyze it step by step.
What we need to do with this event is to handle when someone subscribes/unsubscribes to it from outside. In this case, we want to mirror this to the Internal Timer.
In other words, if someone from outside is having an instance of our ITimer
, he should be able to do something like this t.TimerIntervalElapsed += (sender, dateTime) => { //do something }
.
At this moment, what we should do is internally do something like m_Timer.Elapsed += (sender, elapsedEventArgs) => { //do something }
.
However, we need to keep in mind that the two handlers are not the same as they are actually of different types; TimerIntervalElapsedEventHandler
and ElapsedEventHandler
.
Therefore, what we need to do is to wrap the coming in TimerIntervalElapsedEventHandler
into a new internal ElapsedEventHandler
. This is something we can do.
However, we also need to keep in mind that, at some point, someone might need to unsubscribe a handler from the TimerIntervalElapsedEventHandler
event.
This means that, at this moment, we need to be able to know which ElapsedEventHandler
handler corresponds to that TimerIntervalElapsedEventHandler
handler so that we can unsubscribe it from the Internal Timer.
The only way to achieve this is by keeping track of each TimerIntervalElapsedEventHandler
handler and the newly created ElapsedEventHandler
handler in a dictionary. This way, by knowing the passed in TimerIntervalElapsedEventHandler
handler, we can know the corresponding ElapsedEventHandler
handler.
However, we also need to keep in mind that from outside, someone might subscribe to the same TimerIntervalElapsedEventHandler
handler more than once.
Yes, this is not logical, but still, it is doable. Therefore, for the sake of completeness, for each TimerIntervalElapsedEventHandler
handler, we would keep a list of ElapsedEventHandler
handlers.
In most cases, this list would have only one entry unless in case of a duplicate subscription.
And that’s why you can see this private Dictionary<TimerIntervalElapsedEventHandler, List<ElapsedEventHandler>> m_Handlers = new();
.
public event TimerIntervalElapsedEventHandler TimerIntervalElapsed
{
add
{
var internalHandler =
(ElapsedEventHandler)((sender, args) => { value.Invoke(sender, args.SignalTime); });
if (!m_Handlers.ContainsKey(value))
{
m_Handlers.Add(value, new List<ElapsedEventHandler>());
}
m_Handlers[value].Add(internalHandler);
m_Timer.Elapsed += internalHandler;
}
remove
{
m_Timer.Elapsed -= m_Handlers[value].Last();
m_Handlers[value].RemoveAt(m_Handlers[value].Count - 1);
if (!m_Handlers[value].Any())
{
m_Handlers.Remove(value);
}
}
}
In the add
, we are creating a new ElapsedEventHandler
, adding a record in m_Handlers
the dictionary mapping this to TimerIntervalElapsedEventHandler
, and finally subscribing to the Internal Timer.
In the remove
, we are getting the corresponding list of ElapsedEventHandler
handlers, selecting the last handler, unsubscribing it from the Internal Timer, removing it from the list, and removing the whole entry if the list is empty.
Also worth mentioning, the Dispose
implementation.
protected virtual void Dispose(bool disposing)
{
if (m_IsDisposed) return;
if (disposing && m_Handlers.Any())
{
foreach (var internalHandlers in m_Handlers.Values)
{
if (internalHandlers?.Any() ?? false)
{
internalHandlers.ForEach(handler => m_Timer.Elapsed -= handler);
}
}
m_Timer.Dispose();
m_Timer = null;
m_Handlers.Clear();
m_Handlers = null;
}
m_IsDisposed = true;
}
We are unsubscribing all the remaining handlers from the Internal Timer, disposing of the Internal Timer, and clearing the m_Handlers
dictionary.
using BetterTimerApp.Abstractions;
using BetterTimerApp.Implementations;
namespace BetterTimerApp
{
public class Program
{
static void Main(string[] args)
{
var timer = new Timer();
IPublisher publisher = new Publisher(timer, new Implementations.Console());
publisher.StartPublishing();
System.Console.ReadLine();
publisher.StopPublishing();
timer.Dispose();
}
}
}
Here, we are still not doing much. It is almost the same as the old solution.
Running this should end up with something like this:
Now, we have our final design. However, we need to see if this design really can help us cover our Publisher
module with unit tests.
The structure of the solution should look like this:
I am using NUnit and Moq for testing. You can for sure work with your preferred libraries.
using System;
using System.Collections.Generic;
using BetterTimerApp.Abstractions;
namespace BetterTimerApp.Tests.Stubs
{
public enum Action
{
Start = 1,
Stop = 2,
Triggered = 3,
Enabled = 4,
Disabled = 5,
IntervalSet = 6
}
public class ActionLog
{
public Action Action { get; }
public string Message { get; }
public ActionLog(Action action, string message)
{
Action = action;
Message = message;
}
}
public class TimerStub : ITimer
{
private bool m_Enabled;
private double m_Interval;
public event TimerIntervalElapsedEventHandler TimerIntervalElapsed;
public Dictionary<int, ActionLog> Log = new();
public bool Enabled
{
get => m_Enabled;
set
{
m_Enabled = value;
Log.Add(Log.Count + 1,
new ActionLog(value ? Action.Enabled : Action.Disabled, value ? "Enabled" : "Disabled"));
}
}
public double Interval
{
get => m_Interval;
set
{
m_Interval = value;
Log.Add(Log.Count + 1, new ActionLog(Action.IntervalSet, m_Interval.ToString("G17")));
}
}
public void Start()
{
Log.Add(Log.Count + 1, new ActionLog(Action.Start, "Started"));
}
public void Stop()
{
Log.Add(Log.Count + 1, new ActionLog(Action.Stop, "Stopped"));
}
public void TriggerTimerIntervalElapsed(DateTime dateTime)
{
OnTimerIntervalElapsed(dateTime);
Log.Add(Log.Count + 1, new ActionLog(Action.Triggered, "Triggered"));
}
protected void OnTimerIntervalElapsed(DateTime dateTime)
{
TimerIntervalElapsed?.Invoke(this, dateTime);
}
public void Dispose()
{
Log.Clear();
Log = null;
}
}
}
What we can notice here:
We defined the Action
enum to be used while logging the actions performed through our Timer stub. This would be used later to assert the internal actions performed.
Also, we defined the ActionLog
class to be used for logging.
We defined the TimerStub
class as a stub of ITimer
. We would use this stub later when testing the Publisher
module.
The implementation is simple. Worth to mention that we added an extra public void TriggerTimerIntervalElapsed(DateTime dateTime)
method so that we can trigger the stub manually within a unit test.
We can also pass in the expected value of the dateTime
so that we have a known value to assert against.
using System;
using BetterTimerApp.Abstractions;
using BetterTimerApp.Implementations;
using BetterTimerApp.Tests.Stubs;
using Moq;
using NUnit.Framework;
using Action = BetterTimerApp.Tests.Stubs.Action;
namespace BetterTimerApp.Tests.Tests
{
[TestFixture]
public class PublisherTests
{
private TimerStub m_TimerStub;
private Mock<IConsole> m_ConsoleMock;
private Publisher m_Sut;
[SetUp]
public void SetUp()
{
m_TimerStub = new TimerStub();
m_ConsoleMock = new Mock<IConsole>();
m_Sut = new Publisher(m_TimerStub, m_ConsoleMock.Object);
}
[TearDown]
public void TearDown()
{
m_Sut = null;
m_ConsoleMock = null;
m_TimerStub = null;
}
[Test]
public void ConstructorTest()
{
Assert.AreEqual(Action.Enabled, m_TimerStub.Log[1].Action);
Assert.AreEqual(Action.Enabled.ToString(), m_TimerStub.Log[1].Message);
Assert.AreEqual(Action.IntervalSet, m_TimerStub.Log[2].Action);
Assert.AreEqual(1000.ToString("G17"), m_TimerStub.Log[2].Message);
}
[Test]
public void StartPublishingTest()
{
// Arrange
var expectedDateTime = DateTime.Now;
m_ConsoleMock
.Setup
(
m => m.WriteLine
(
It.Is<DateTime>(p => p == expectedDateTime)
)
)
.Verifiable();
// Act
m_Sut.StartPublishing();
m_TimerStub.TriggerTimerIntervalElapsed(expectedDateTime);
// Assert
ConstructorTest();
m_ConsoleMock
.Verify
(
m => m.WriteLine(expectedDateTime)
);
Assert.AreEqual(Action.Start, m_TimerStub.Log[3].Action);
Assert.AreEqual("Started", m_TimerStub.Log[3].Message);
Assert.AreEqual(Action.Triggered, m_TimerStub.Log[4].Action);
Assert.AreEqual(Action.Triggered.ToString(), m_TimerStub.Log[4].Message);
}
[Test]
public void StopPublishingTest()
{
// Act
m_Sut.StopPublishing();
// Assert
ConstructorTest();
Assert.AreEqual(Action.Stop, m_TimerStub.Log[3].Action);
Assert.AreEqual("Stopped", m_TimerStub.Log[3].Message);
}
[Test]
public void FullProcessTest()
{
// Arrange
var expectedDateTime1 = DateTime.Now;
var expectedDateTime2 = expectedDateTime1 + TimeSpan.FromSeconds(1);
var expectedDateTime3 = expectedDateTime2 + TimeSpan.FromSeconds(1);
var sequence = new MockSequence();
m_ConsoleMock
.InSequence(sequence)
.Setup
(
m => m.WriteLine
(
It.Is<DateTime>(p => p == expectedDateTime1)
)
)
.Verifiable();
m_ConsoleMock
.InSequence(sequence)
.Setup
(
m => m.WriteLine
(
It.Is<DateTime>(p => p == expectedDateTime2)
)
)
.Verifiable();
m_ConsoleMock
.InSequence(sequence)
.Setup
(
m => m.WriteLine
(
It.Is<DateTime>(p => p == expectedDateTime3)
)
)
.Verifiable();
// Act
m_Sut.StartPublishing();
m_TimerStub.TriggerTimerIntervalElapsed(expectedDateTime1);
// Assert
ConstructorTest();
m_ConsoleMock
.Verify
(
m => m.WriteLine(expectedDateTime1)
);
Assert.AreEqual(Action.Start, m_TimerStub.Log[3].Action);
Assert.AreEqual("Started", m_TimerStub.Log[3].Message);
Assert.AreEqual(Action.Triggered, m_TimerStub.Log[4].Action);
Assert.AreEqual(Action.Triggered.ToString(), m_TimerStub.Log[4].Message);
// Act
m_TimerStub.TriggerTimerIntervalElapsed(expectedDateTime2);
// Assert
m_ConsoleMock
.Verify
(
m => m.WriteLine(expectedDateTime2)
);
Assert.AreEqual(Action.Triggered, m_TimerStub.Log[5].Action);
Assert.AreEqual(Action.Triggered.ToString(), m_TimerStub.Log[5].Message);
// Act
m_Sut.StopPublishing();
// Assert
Assert.AreEqual(Action.Stop, m_TimerStub.Log[6].Action);
Assert.AreEqual("Stopped", m_TimerStub.Log[6].Message);
}
}
}
Now, as you can see, we have full control, and we can easily cover our Publisher
module with unit tests.
If we calculate the coverage, we should get this:
As you can see, the Publisher
module is 100% covered. For the rest, this is out of scope of this article, but you can simply cover it if you follow the approach in the article
You can do it. It is just a matter of splitting large modules into smaller ones, defining your abstractions, getting creative with tricky parts, and then you are done.
If you want to train yourself more, you can check my other articles about some Best Practices.
That’s it, hope you found reading this article as interesting as I found writing it.
Also published here