DNN Development Tips:11 - Testable Controllers–using the ServiceLocator class.

Last Modified: Oct 20 2021
Oct 24 2014

In this DNN Development Tips series I have blogged quite a bit about testing.  But how can we ensure our classes are testable?

Historically, DNN has used a “Repository” style – a lightweight Entity class – usually suffixed with Info, and a Repository class which has traditionally used Controller as a suffix.  This naming strategy was present in the iBuySpy Portal Starter-Kit upon which DNN is based.  Thus, for instance, if the business layer needs to model Task objects, there will be a TaskInfo entity and a TaskController repository class.

Back in 2002/3 when DNN was first created, developers were not so concerned about whether they could write Unit Tests, so the pattern of use was that whenever a “controller” was needed it was constructed on the fly.  If that controller in turn needed to call a 2nd controller then it was also constructed on the fly.

The problem, from a modern perspective is that it is difficult to write Unit Tests using this pattern, as there are unknown dependencies that cannot be mocked or faked.

Dependency Injection

One of the ways to solve this is to use Dependency Injection.  This pattern is one (the D) of the SOLID Principles of Object Oriented Design espoused by “Uncle” Bob Martin and others (see DNN Developer Tips #1).  The Dependency Injection Principle says that any dependencies that a class has should be “injected”, usually through the Constructor.

So if ControllerA has a dependency on ControllerB then ControllerA’s constructor should look like:

   1:  private ControllerB _controllerB
   2:   
   3:  public ControllerA(ControllerB controllerB)
   4:  {
   5:      _controllerB = controllerB.
   6:  }

Actually to enable effective Unit Testing the constructor should be improved to inject the dependency as an Interface.

   1:  private IControllerB _controllerB
   2:   
   3:  public ControllerA(IControllerB controllerB)
   4:  {
   5:      _controllerB = controllerB.
   6:  }

The class that is constructing a ControllerA would need to also construct a concrete implementation (ControllerB) of IControllerB and pass it as a parameter.

Service Location

An alternative approach to Dependency Injection is to use Service Location, and this is the approach we have used to retrofit DNN for effective Unit Testing.  This is best demonstrated by using an example.

Listing 1: An example of a Controller using the ServiceLocator pattern

   1:  public partial class TabController 
   2:       : ServiceLocator 
   3:       , ITabController
   4:      {
   5:          protected override Func GetFactory()
   6:          {
   7:              return () => new TabController();
   8:          }
   9:   
  10:  .....
  11:   
  12:  }

The code snippet in Listing 1 shows the basic setup to create a Controller using this pattern.  The TabController inherits from the ServiceLocator base class as well as an interface ITabController.  In DNN 7.3 all the non-static methods of TabController were extracted into the ITabController interface.

To use the TabController class in a testable manner the ServiceLocator provides an Instance property (similar to the Instance method used for DNN’d providers).

TabController.Instance.GetTabsByPortal(PortalID)

The Instance property returns the current instance of the ITabController interface.  If the base ServiceLocator does not have a current instance it calls the GetFactory method to instantiate a new instance.

So how does this make the TabController class testable?

The ServiceLocator class also has a SetTestableInstance method.  This allows Unit Test writers to create a Mock ITabController and set it as the instance to return.

TabController.SetTestableInstance(_mockTabController.Object);

Now any method that calls the INstance method returns the testable Mock, thus improving the testability.

Disclaimer

The opinions expressed herein are my own personal opinions and do not represent my employer's view in anyway.

Tags