How to easily use the observer pattern to loosely couple tests to the logging engine

One important aspect of any testing is logging good information about what’s going on in the tests. Tests are made up of many modules. In a lot of cases one logging engine is packaged with the test harness. All of the components downstream from the harness instantiate the logging engine. Sometimes they use an interface to help keep things generic. Sometimes they directly instantiate the logging engine. If your generic library is multi-level, each object in the hierarchy must instantiate the logging engine and use it.

Diagram of a standard way to do test logging

Limitations of the standard way of doing business.

This architecture isn’t a bad one. But it has some limitations. Changing logging engines may mean touching code in every module. If you have a generic interface this is easier of course. You still can’t do things like easily create console or GUI applications based on your generic library. You would have to import the logging engine, and maybe even subvert it to do things like write messages to the console.

The observer pattern to the rescue

From a design pattern point of view, the best way to overcome these problems is to create a generic pipeline of events. The generic library exposes events. The tests subscribe to those events and send them to the log engine. If your library has several levels, you just construct a “pipeline” to pass messages up the chain.

The observer pattern works well, but in practice it’s unwieldy. You have to design your message classes before your observers. The message classes are tightly coupled and have to be instantiated in every level of the observer chain. Luckily C# generics offer us a way to gain the benefits of the pattern without incurring the coding and design overhead.

 

Diagram of doing test logging with the observer pattern

 

Diagram of “skinning” the exact same code with a console interface (could even use the same built assembly)

 

 

Message event factory is handy for logging, but can also be used to send event with arbitrarily complex messages. You just need a struct or class to contain the data you want to pass.

Message Event Factory is a generic implementation of the observer pattern

You can easily drop it into any code that wants to raise events. You can subscribe to those events easily as well. In fact Intellesense makes this almost as easy as hitting the tab button twice.

Sample code for instantiating a string type message

// Your library class.

class lib

{

    //Instantiate a MessageEventer with the desired type. String in this case.

    MessageEventer<string> stringEventer = new MessageEventer<string>();

 

    // Exposes the MessageEventer this class uses.

    public MessageEventer<string> StringEventer

    {

 

        get { return stringEventer; }

    }

 

    // Represents a public method a library class would have.

    public void DoThings()

    {

        //This code would be going about it's business and providing updates via the RaiseEvent calls.

        stringEventer.RaiseEvent("string one");

        stringEventer.RaiseEvent("string two");

        stringEventer.RaiseEvent("string three");

    }

}

 

Sample code for the observer

// The interface class. Could have been a windows interface or a test.       

class app

{

 

    // Program Entry Point.

 

    public static void Main(string[] args)

    {

        //instantiate you library, might be in a DLL.

        lib lib = new lib();

        //sign up to recieve events.

        lib.StringEventer.MessageSent += new MessageEventer<string>.MessageEventHandler(stringEventer_MessageSent);

 

        //Call your library functions.

        lib.DoThings();

    }

 

    //Intercept the raised events and do something (in this case log them to Console).

    static void stringEventer_MessageSent(MessageEventer<string> m, MessageEventArgs<string> e)

    {

        Console.WriteLine(e.Data);

    }

}

 

Message Event Factory code

This is the entire assembly, it’s very simple, but very powerful. If you wanted to implement the observer pattern explicitly in your classes, each one would need about half of this code embedded. Note that could could inherit this code into your classes. In practice it works better as a “has-a” relationship rather than “is-a”.

using System;

using System.Runtime.Serialization;

 

namespace Microsoft.LiveMeeting.Service.Test.Tools

{

 

    /// <summary>

    /// This class is a simple generic event factory.

    /// </summary>

    /// <remarks>

    /// It's designed to allow library code to easily raise

    /// messages to interested client code without throwing exceptions.

    /// This allows the library code to stay interface neutral and the

    /// top level code can decide how to log/present the messages.

    /// </remarks>   

    public class MessageEventer<T>

    {

        /// <summary>

        /// Use this handler to sign up for event notifications.

        /// </summary>

        public event MessageEventHandler MessageSent;

 

        /// <summary>

        /// This is the delegate needed to send the messages. You shouldn't need to access it directly.

        /// </summary>

        /// <param name="m"></param>

        /// <param name="e"></param>

        public delegate void MessageEventHandler(MessageEventer<T> m, MessageEventArgs<T> e);

 

        private void raiseEvent(MessageEventArgs<T> e)

        {

            //Null means no one has signed up yet.

            if(MessageSent != null)

            {

                MessageSent(this, e);

            }

        }

 

 

        /// <summary>

        /// sends the data to all the signed up observers.

        /// </summary>

        /// <param name="data"></param>

        public void RaiseEvent(T data)

        {

            MessageEventArgs<T> e = new MessageEventArgs<T>(data);

            raiseEvent(e);

        }

    }

 

   

 

    /// <summary>

    /// Event Argument class used in MessageEventer.

    /// </summary>

    public class MessageEventArgs<T> : EventArgs

    {

       

        private readonly T data = default(T);

 

        /// <summary>

        /// Creates a message event object with the desired data.

        /// </summary>       

        public MessageEventArgs(T data )

        {

            if (data == null)

            {

                throw new ArgumentException("Message cannot be null.");

            }

            this.data = data;

        }

       

        #region read only properties

 

        /// <summary>

        /// String message sent from the source.

        /// </summary>

        public T Data

        {

            get { return data; }       

        }

 

 

        #endregion

 

    }

}