Custom EventListener Testing

Introduction

The previous post has looked into creating custom EventCounters and testing them. In this post I will look into how to unit test custom in-process event listeners. The previous post has given an overview on the key classes and their behavior for event counters in .net core. Reading that post will help with the understanding of this post.

Custom EventListener

First, I will create a custom event listener, which listens for periodic updates from cpu-usage EventCounter from System.Runtime event source. I would like to test that the custom listener logic within this listener works as expected.

There are two ways to subsribe to cpu-usage counter. In both we start by deriving from EventListener class. We can decide to create a generic event listener or one specific to a given EventSource. In case of built-in events generic event listeners are more complex, because to enable events for a given event source EnableEvents() method should be called with the given event source passed in as an argument. EventSource references are typically received in the OnEventSourceCreated() method.

When creating a SystemRuntimeEventListener we preferably would like to reuse and subscribe to multiple counters/sources and not just a set of hardcoded ones. For this reason, a GenericEventListener is created instead. There could be other approaches to make an vanialla EventListener testable, but in this case I am looking for a way that makes it more re-usabe and testable at the same time.

public sealed class GenericEventListener : EventListener
{
  private string _counterName;
  private string _eventSourceName;
  private Guid? _eventSourceId;
  private float _intervalSec;

  public double Value { get; private set; }

  public GenericEventListener(string eventSourceName, string counterName, Guid? eventSourceId = null, float intervalSec = 1)
  {
    _eventSourceName = eventSourceName ?? throw new ArgumentNullException(nameof(eventSourceName));
    _counterName = counterName ?? throw new ArgumentNullException(nameof(counterName));
    _eventSourceId = eventSourceId;
    _intervalSec = intervalSec;
    foreach (var eventSource in EventSource.GetSources())
      OnEventSourceCreated(eventSource);
  }

  public void Stop() => _isStarted = false;

  protected override void OnEventSourceCreated(EventSource eventSource)
  {
    if (eventSource.Name == _eventSourceName && (_eventSourceId == null || _eventSourceId == eventSource.Guid))
    {
      EnableEvents(eventSource, EventLevel.LogAlways, EventKeywords.All, new Dictionary<string, string> { { "EventCounterIntervalSec", _intervalSec.ToString() } });
      _isStarted = true;
    }
  }

  protected override void OnEventWritten(EventWrittenEventArgs eventData)
  {
    if (eventData.Payload == null || eventData.Payload.Count == 0)
      return;
    if (eventData.Payload[0] is IDictionary<string, object> eventPayload && eventPayload.TryGetValue("Name", out var nameData) && nameData is string name && name == _counterName)
    {
      if (eventPayload.TryGetValue("Mean", out var value) && value is double dValue)
      {
        Value = dValue;
        base.OnEventWritten(eventData);
      }
    }
  }
}

Overriding OnEventSourceCreated() method we can get a reference to the event source object, but it might be called before the listener's constructor completes. The only way to get a reference to event sources is to re-iterate them.

The other key call here is the base.OnEventWritten(eventData); which invokes the base and fires the related C# event. This is important from the test's point of view as this is can be a test hook.

This listener has a Value property, once the awaited counter value received, it sets the mean value to this property. There is no specific purpose with this value, for now we could have any custom logic instead of this. The next section shows how we can test and assert that this custom logic behaves as expected.

Testing Custom EventListners

To test such an EventListener class, I will use the xUnit test framework. For the tests 2 helper classes are added: a generic TestEventSource and a static class for extension methods for GenericEventListenerExtension. I also disable running tests parallel.

TestEventSource is a simple event source to fire events. Note, that the event source name and counter name can be arbitary, because the custom event listener is generic enough to handle all names.

[EventSource(Name = "TestEvents")]
public sealed class TestEventSource : EventSource
{
  private readonly EventCounter _testCounter;

  public TestEventSource()
  {
    _testCounter = new EventCounter("sudoku", this);
  }

  public void Fire(double value) => _testCounter.WriteMetric(value);
}

The second helper class GenericEventListenerExtension, subscribes to the EventWritten C# events, and uses the passed in predicate to decide if we have received all desired events.

public static class GenericEventListenerExtension
{
  public static Task<IEnumerable<IDictionary<string, object>>> ListenAsync(this GenericEventListener listener, Func<IDictionary<string, object>, bool> isLastEvent)
  {
    List<IDictionary<string, object>> eventPayloads = new List<IDictionary<string, object>>();
    TaskCompletionSource<IEnumerable<IDictionary<string, object>>> tcs = new TaskCompletionSource<IEnumerable<IDictionary<string, object>>>();
    listener.EventWritten += (object sender, EventWrittenEventArgs eventData) =>
    {
      if (eventData.Payload != null && eventData.Payload.Count != 0 && eventData.Payload[0] is IDictionary<string, object> eventPayload)
      {
        eventPayloads.Add(eventPayload);
        if (isLastEvent(eventPayload))
        {
          tcs.TrySetResult(eventPayloads);
          listener.Stop();
        }
      }
    };
    return tcs.Task;
  }
}

This helper method collects the events captured by the listener. It also completes a task from TaskCompletionSource once a given predicate is true. The task returns all the collected events, so the user of it may assert each one of them. Multiple events may be received, as events are fired on a separate thread (to the one where an event source creates them). When a listener subscribes to an EventSource we may get an initial event with with all counter values set to 0, hence typically multiple events must be collected.

Using the above helpers, a test may look as simple:

[Theory]
[InlineData(1)]
[InlineData(5)]
public async Task Event_IsCaptured_ByListener(int value)
{
  using var sut = new GenericEventListener("TestEvents", "sudoku", intervalSec: 0.1f);
  var messagesRecived = sut.ListenAsync(x => x.TryGetValue("Name", out var counterName) && (string)counterName == "sudoku" && x.TryGetValue("Mean", out var meanValue) && (double)meanValue == value);
  using var es = new TestEventSource();
  es.Fire(value);
  var eventPayloads = await messagesRecived;
  // Optionally assert all events in eventPayloads
  // Assert the Value property of the listener
  Assert.Equal(value, sut.Value);
}

The most important fact is that both the listener and the event source are enclosed by a using block. ListenAsync() sets the predicate, which is looking for a given event to be captured. Then a new TestEventSource is created and an event is fired. When the Task returned by ListenAsync() method is awaited, the events can be observed and the behavior of the GenericEventListener can be asserted.

Conclusion

This is not a final solution, it gives some initial directions on how custom event listeners may be tested. This post only covers one happy case, to test further use-cases we may need minor tweaks with the helper classes. For example this solution does not cover timeouts, when events are not received. Testing eventing is inherently difficult, because all events are fired on a separate thread compared to the source's thread. There are no or very little hooks we can use for testing to avoid waiting for arbitary durations. Good test hooks must be found for each behavior of the listener to be tested.