paint-brush
The Best Way to Use Timers in .NET C#by@ahmedtarekhasan
5,251 reads
5,251 reads

The Best Way to Use Timers in .NET C#

by Ahmed Tarek HasanMarch 29th, 2023
Read on Terminal Reader
Read this story w/o Javascript

Too Long; Didn't Read

Using System.Timers.Timer in your 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. By the end, you would be able to achieve 100% coverage of your modules.
featured image - The Best Way to Use Timers in .NET C#
Ahmed Tarek Hasan HackerNoon profile picture

How to Have Full Control Over the Timer and Be Able to Reach 100% Coverage With Unit Tests

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.


Photo by Lina Trochez on Unsplash

The Approach

This is how we are going to approach our solution:


  1. Come up with a very simple example to work on.


  2. Start with the simple bad solution.


  3. Keep trying to enhance it till we reach the final format.


  4. Summarizing the lessons learned through our journey.


Photo by James Harrison on Unsplash

The Example

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:


Image by Ahmed Tarek


As you can see, it is simple in terms of requirements, nothing fancy.


Photo by Mikael Seegen on Unsplash, adjusted by Ahmed Tarek

Disclaimer

  1. Some best practices would be ignored/dropped in order to drive the main focus to the other best practices targeted in this article.


  2. 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 How to Fully Cover .NET C# Console Application With Unit Tests.


  3. 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.


Photo by Maria Teneva on Unsplash, adjusted by Ahmed Tarek

Bad Solution

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:


Image by Ahmed Tarek


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 When Not To Use DI, IoC, and IoC Containers in .NET C#.


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:


Image by Ahmed Tarek


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.


Photo by Carson Masterson on Unsplash, adjusted by Ahmed Tarek

Good Solution

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:


Image by Ahmed Tarek


It is the same UsingTimer solution with a new Console BetterTimerApp project.


IConsoleIPublisher, and Console would be the same.

ITimer

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:


  1. We defined the new delegate TimerIntervalElapsedEventHandler. This delegate represents the event to be raised by our ITimer.


  2. 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.


  3. 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.


  4. 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.


  5. Also, note that ITimer extends the IDisposable.



Publisher

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.

Timer

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:


  1. Internally, we are using System.Timers.Timer.


  2. We applied the IDisposable design pattern. That’s why you can see the private bool m_IsDisposedpublic void Dispose()protected virtual void Dispose(bool disposing), and ~Timer().


  3. 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.


  4. For public bool Enabledpublic double Intervalpublic void Start(), and public void Stop(), we are just delegating the implementation to the Internal Timer.


  5. For public event TimerIntervalElapsedEventHandler TimerIntervalElapsed, this is the most important part; so let’s analyze it step by step.


  6. 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.


  7. 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 }.


  8. At this moment, what we should do is internally do something like m_Timer.Elapsed += (sender, elapsedEventArgs) => { //do something }.


  9. 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.


  10. Therefore, what we need to do is to wrap the coming in TimerIntervalElapsedEventHandler into a new internal ElapsedEventHandler. This is something we can do.


  11. However, we also need to keep in mind that, at some point, someone might need to unsubscribe a handler from the TimerIntervalElapsedEventHandler event.


  12. 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.


  13. 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.


  14. However, we also need to keep in mind that from outside, someone might subscribe to the same TimerIntervalElapsedEventHandler handler more than once.


  15. 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.


  16. In most cases, this list would have only one entry unless in case of a duplicate subscription.


  17. 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.

Program

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:


Image by Ahmed Tarek


Photo by Testalize.me on Unsplash, adjusted by Ahmed Tarek

Time For Testing, The Moment of Truth

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:


Image by Ahmed Tarek


I am using NUnit and Moq for testing. You can for sure work with your preferred libraries.

TimerStub

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:


  1. 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.


  2. Also, we defined the ActionLog class to be used for logging.


  3. We defined the TimerStub class as a stub of ITimer. We would use this stub later when testing the Publisher module.


  4. 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.


  5. We can also pass in the expected value of the dateTime so that we have a known value to assert against.

PublisherTests

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:


Image by Ahmed Tarek


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 How to Fully Cover .NET C# Console Application With Unit Tests.


Photo by Jingda Chen on Unsplash, adjusted by Ahmed Tarek

Final Words

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