Поделиться через


Testing Classes in Isolation

Typical Goals

When you want to run unit tests on your code, dependencies on internal or external classes can introduce problems. At best, including these service implementations will introduce a degree of complexity to the test process. At worst, the external code may not function as expected or may not even have been developed yet.

The easiest way to support unit testing is to use the service locator to remove direct dependencies from your classes. Assuming that you designed your classes to use the service locator in this way, you can use the following approach to replace your dependencies with mock objects or stub implementations and test specific behaviors of your class in isolation.

Solution

To isolate your code, you can configure the SharePoint Service Locator to return mock objects for specified interfaces instead of full implementations. To do this, you must create a new service locator instance, configure your type mappings, and then replace the current service locator instance with your new service locator instance. It is possible to create your own mock service locator that implements the IServiceLocator interface, but it is generally easier to use an existing service locator implementation such as the ActivatingServiceLocator class.

The reason you should create a new service locator instance, rather than simply using the current instance, is that instantiating the default service locator requires your code to run in a SharePoint environment. This is because the SharePoint Service Locator attempts to retrieve type mappings from the local SPFarm and SPSite objects when you call SharePointServiceLocator.GetCurrent(). The SharePoint object model is unavailable if you perform an isolated unit test.

In unit testing scenarios you should configure the service locator to instantiate your mock objects as singleton services, so that every request to the service locator returns the same object. For more information on using the ActivatingServiceLocator class, see Creating a New Service Locator Instance and Adding Type Mappings.

Using the SharePoint Service Locator for Unit Tests

The following examples are taken from the SharePoint Logger, a component in the SharePoint Guidance library that you can use to log information to the Windows event log and the Unified Logging Service (ULS) trace log. The examples have been simplified to distill the key points of interest for unit testing. You can read about the SharePoint Logger in more detail in the SharePoint Logger chapter.

Creating Testable Classes

At a basic level, the SharePointLogger class relies on implementations of two key interfaces: ITraceLogger and IEventLogLogger. The default implementations of these classes log information to the ULS trace log and the Windows Event log, respectively. In order to unit test the SharePointLogger class, we need to be able to substitute mock implementations of ITraceLogger and IEventLogLogger without editing and recompiling the SharePointLogger class. To support this approach, rather than referencing implementations of ITraceLogger and IEventLogLogger directly, the SharePointLogger class uses the SharePoint Service Locator to retrieve the registered implementations.

In this case we're interested in what happens when you call the TraceToDeveloper method. Our unit test verifies that this method writes trace information but does not write event log information. The following code example shows the relevant excerpts from the SharePointLogger class and its base class, BaseLogger. When you call the TraceToDeveloper method:

  • The TraceToDeveloper method builds a trace message and calls the WriteToDeveloperTrace method.
  • The WriteToDeveloperTrace method uses the SharePoint Service Locator to retrieve an instance of ITraceLogger, and then calls the ITraceLogger.Trace method.
public abstract class BaseLogger : ILogger
{     
   public void TraceToDeveloper(string message, int eventId, 
                                TraceSeverity severity, string category)
   {
      WriteToDeveloperTrace(
                    BuildTraceMessage(message, eventId, severity, category), 
                    eventId, severity, category);
   }

   protected abstract void WriteToDeveloperTrace(string message, int eventId,     
                                TraceSeverity severity, string category);
}

public class SharePointLogger : BaseLogger
{
   private ITraceLogger traceLogger;
   public ITraceLogger TraceLogger
   {
   get
      {
         if (traceLogger == null)
         {
            traceLogger =   
               SharePointServiceLocator.GetCurrent().GetInstance<ITraceLogger>();
         }
         return traceLogger;
      }
   }

   protected override void WriteToDeveloperTrace(string message, int eventId,  
                                      TraceSeverity severity, string category)
   {
      try
{
TraceLogger.Trace(message, eventId, severity, category);
}
catch (Exception ex)
{
         AttemptToWriteTraceExceptionToEventLog(ex, message);
}
   }
}

Notice that at no point in the previous code example were specific implementations of ITraceLogger referenced directly. This means that you can provide alternative implementations of ITraceLogger without editing your original methods.

Creating Mock Objects

At this point, we've used the SharePoint Service Locator to decouple the SharePointLogger class from specific implementations of ITraceLogger and IEventLogLogger. The next stage is to create mock implementations of ITraceLogger and IEventLogLogger that we can supply to the SharePointLogger class for the purposes of unit testing. In this case, our mock implementations simply build a generic list that we can interrogate from our test class. The following code example shows the mock implementations of ITraceLogger and IEventLogLogger.

class MockTraceLogger : ITraceLogger
{
   public List<string> Messages = new List<string>();

   public string Message;
   public string Category;
   public int EventID;
   public TraceSeverity Severity;

   public void Trace(string message, int eventId, TraceSeverity severity, 
                     string category)
   {
      this.Messages.Add(message);

      this.Message = message;
      this.Category = category;
      this.EventID = eventId;
      this.Severity = severity;
   }
}

class MockEventLogger : IEventLogLogger
{
   public List<string> Messages = new List<string>();

   public string Message;
   public string Category;
   public int EventID;
   public EventLogEntryType Severity;

   public void Log(string message, int eventId, EventLogEntryType severity, 
                   string category)
   {
      this.Messages.Add(message);

      this.Message = message;
      this.Category = category;
      this.EventID = eventId;
      this.Severity = severity;
   }
}

Configuring the Unit Test

At this point, we've created a testable SharePointLogger class and we've created some mock implementations of ITraceLogger and IEventLogLogger for use in unit tests. The final stage is to create the test class itself. The test method consists of three key sections:

  • Arrange. In this section we set up the SharePoint Service Locator. We replace the default current service locator with a new instance of the ActivatingServiceLocator class. We register our mock classes as the default implementations of ITraceLogger and IEventLogLogger. Finally we use the service locator to instantiate our mock classes.
  • Act. In this section we perform the action that we want to test. We create a new instance of SharePointLogger and we call the TraceToDeveloper method.
  • Assert. In this section we use various assert statements to verify that the SharePointLogger class behaved as expected.

The following code example shows the relevant parts of the test class.

[TestClass]
public class SharePointLoggerFixture
{
   private MockTraceLogger traceLogger;
   private MockEventLogger eventLogger;

   [TestMethod]
   public void TraceLogsOnlyToTraceLog()
   {
      //Arrange
      ActivatingServiceLocator replaceLocator = new ActivatingServiceLocator();
      SharePointServiceLocator.ReplaceCurrentServiceLocator(replaceLocator);
      
      replaceLocator.RegisterTypeMapping<ITraceLogger, MockTraceLogger>
                                           (InstantiationType.AsSingleton);
      replaceLocator.RegisterTypeMapping<IEventLogLogger, MockEventLogger>
                                           (InstantiationType.AsSingleton);

      traceLogger = SharePointServiceLocator.GetCurrent()
                       .GetInstance<ITraceLogger>() as MockTraceLogger;
      eventLogger = SharePointServiceLocator.GetCurrent()
                       .GetInstance<IEventLogLogger>() as MockEventLogger;

      //Act
      SharePointLogger target = new SharePointLogger();
      target.TraceToDeveloper("Message", 99, TraceSeverity.High, "Category1");

      //Assert
      Assert.IsNull((target.EventLogLogger as MockEventLogger).Message);
      AssertLogData(target.TraceLogger as MockTraceLogger, TraceSeverity.High);

      //Cleanup
      SharePointServiceLocator.Reset();
   }
}

Because we designed the SharePointLogger class for testability and decoupled the class from its dependencies, this entire test process can be conducted without editing or recompiling the SharePointLogger class itself.

Usage Notes

After your test completes, call the SharePointServiceLocator.Reset method. This ensures that the next call to the SharePointServiceLocator.GetCurrent() property creates a new service locator instance. It is recommended that you use the Reset method to return the service locator to its original state as part of the cleanup step for your unit test. This prevents tests from interfering with each other.