Dependency Injection and Abstract Interfaces in C#

If you’ve ever written code, you’ve probably written a unit test.  If you haven’t written unit tests, you should start.

Now.

Really.

Unit tests let you quickly verify that your code is operating in a predictable fashion.  When you make changes down the road, you re-run the same unit tests to make sure nothing broke.

In many cases, you write the unit tests first.  Define what your code will do, decide what objects you will use/make to accomplish that, then write a test.  At this stage, the test will fail – but you have a place to start.  Now write your code until it passes the test and you can be certain your code, however inelegant it might be, is doing what you expect.

Sometimes, though, you don’t write the unit tests first.  You go back days, weeks, even years later and try to add unit tests to your code.  In most cases, this will be next to impossible because of the way the code is written.  A function with direct calls to the database needs to be rewritten.  File access needs to be abstracted into an object.

It’s tricky, but makes your code more maintainable in the long run.

Database Update Web Service

I was recently tasked with updating the data persistence routines in a legacy web service that lacked unit tests.  My first task was to build out the new test scenarios before changing any code – make sure everything meets expectations before changing those expectations.

The Save() method was the powerhouse of the system.  It took in:

  • The ID of the client sending data
  • A checksum password to verify the client
  • A local filesystem path for a log file
  • The IP address of the client

The object containing the Save() method also contained a local data object populated from an XML file posted to the server.

Initially, the method looked something like this:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
public bool Save(Int32 ClientID, String Password, String sAppData,String IPAddress)
{
  try
  {
    if (Password.CompareTo(GetPasscode(ClientID, Agency.Date)) != 0)
    {
      throw new Exception("Invalid ClientID!");
    }

    string Filename = sAppData + "\\" + ClientID.ToString() + ".xml";

    //-----Check to see if file is from yesterday or today-----
    if (File.Exists(Filename))
    {
      FileInfo info = new FileInfo(Filename);
      //----If File date is today, then create ClientID file
      if (info.LastWriteTime.Date.CompareTo(DateTime.Today.Date) == 0)
      {
        int nCount = 1;
        do
        {
          Filename = sAppData + "\\"
            + ClientID.ToString() + "-"
            + nCount.ToString() + ".xml";
          nCount++;
        } while (File.Exists(Filename));
      }
      else //----if FileDate is not today, wipe out all data for that client id
      {
        File.Delete(Filename);

        //---Delete all iterations---
        String[] Files = Directory.GetFiles(sAppData + "\\", ClientID.ToString() + "-*.xml");
        foreach (String file in Files)
          File.Delete(file);
      }
    }

    if (!Save(Filename))
      throw new Exception("Unable to Save File!");

    File.AppendAllText(sAppData + "\\UploadSysInfo.log", "Pre New Data\r\n");
    UploadInfoDataContext data = new UploadInfoDataContext();
    File.AppendAllText(sAppData + "\\UploadSysInfo.log", "Post New Data\r\n");
    data.SaveToDB(this, IPAddress);
    File.AppendAllText(sAppData + "\\UploadSysInfo.log", "Post SaveToDB\r\n");
  }
  catch(Exception e)
  {
    File.AppendAllText(sAppData + "\\UploadSysInfo.log", "Error: " + e.Message + "\r\n");
    throw;
  }
  return true;
}

Unfortunately, you can’t just invoke this method and test that it works.  The method talks to the file system directly, then calls a data-heave SaveToDB() method that talks directly to several SQL stored procedures.  Invoking this version of the method directly from a test routine will corrupt your data and cause a while host of other unintended effects.

Instead, we can abstract what dependencies the system does have – primarily the file system and database.  By wrapping that access in secondary objects, we can mock those objects and inject our mocks into the system.

Mocking is a fancy way of saying we override the default behavior and prevent the code from actually touching the file system or database by short-circuiting it in a specifi way.

First, we change the signature of the Save() method so that it also takes in an object of type IStatusLog.  This is an object implementing a specific interface for writing to a static text log:

1
2
3
4
public interface IStatusLog
{
  void Write(string Message);
}

Our test code passes in a fake object that implements IStatusLog but doesn’t actually do anything.  Any calls to member methods on the mocked object will go into the abyss.  The program won’t crash, and our test will actually test what we want to pass.

We also add an IXmlDataWriter object to wrap routines that write our posted XML data to disk and an IUploadInfoDataContext interface that wraps our database access methods.  In normal use (i.e. production), the system creates real objects for these interfaces and passed them to our worker method:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
public bool Save(Int32 ClientID, String Password, String sAppData, String IPAddress, IStatusLog statusLog)
{
  this.Log = statusLog;

  this.Log.Write("Pre New Data");
  IUploadInfoDataContext data = new UploadInfoDataContext();
  this.Log.Write("Post New Data");

  return this.Save(
    ClientID,                              // ClientID
    Password,                              // Password
    sAppData,                              // sAppData
    IPAddress,                             // IPAddress
    statusLog,                             // statusLog
    new XmlDataWriter(sAppData, ClientID), // writer
    data                                   // data
  );
}

But the real work is done in another method, an overload of Save() that takes in our objects that implement IStatusLog, IXmlDataWriter, and IUploadInfoDataContext.  In production, these are real objects that touch the file system and database.  In testing, they’re mocks that return whatever values we need them to return.

But since both cases use the same interfaces, they expose the same methods and properties.  We can write our Save() method tied to these dependencies and rely on it both in production and in testing.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
public bool Save(Int32 ClientID, String Password, String sAppData,
  String IPAddress, IStatusLog statusLog, IXmlDataWriter writer,
  IUploadInfoDataContext data)
{
  this.Log = statusLog;

  try
  {
    if (Password.CompareTo(GetPasscode(ClientID, Agency.Date)) != 0)
    {
      throw new ApplicationException("Invalid ClientID!");
    }

    // Save object to XML file
    if (!writer.WriteData(this)) throw new ApplicationException("Unable to Save File!");

    data.SaveToDB(this, IPAddress, this.Log);
    this.Log.Write("Post SaveToDB");
  }
  catch (ApplicationException e)
  {
    this.Log.Write("Error: " + e.Message);
    throw;
  }
  return true;
}

This method is the one we’re testing.  It’s the one that contains the actual logic to validate client data, save our passed object to disk, and update the database.  Everything else has been merely abstracting the underlying framework in such a way that we can inject phoney dependencies in a test environment.

This test method, for example, passes in a mocked log writer, a mocked data writer, and a mocked database context.  However it still validates that the client ID makes it through our password check, that the mock database records data, and that the Send() method returns true.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
[TestMethod]
public void Save_Returns_True_After_Writing_To_Database()
{
  Mock<IXmlDataWriter> dataWriter = new Mock<IXmlDataWriter>();
  Mock<IStatusLog> statusLog= new Mock<IStatusLog>();
  Mock<IUploadInfoDataContext> dataContext = new Mock<IUploadInfoDataContext>();

  UploadInfo info = new UploadInfo();
  info.Agency = new AgencyInfo()
  {
    Date = "12/1/2012"
  };

  dataWriter.Setup(x => x.WriteData(info)).Returns(true);
  dataContext.Setup(x => x.SaveToDB(info, It.IsAny<string>(), It.IsAny<IStatusLog>())).Returns(true);

  bool status = info.Save(
    28,                // ClientID
    "7DC1cC1",         // Password
    "AppData",         // sAppData
    "127.0.0.1",       // IPAddress
    statusLog.Object,  // statusLog
    dataWriter.Object, // writer
    dataContext.Object // data
  );

  Assert.IsTrue(status);
}

Abstracting dependencies into interfaces is a valuable tool in any application developer’s toolbox.  It just so happens that the practice is somewhat more elegant in C# than other languages.  But understanding how, when, and why to use interfaces is a lesson every developer should learn.

About Eric

Eric Mann is a writer, web developer, and outdoorsman living in the Pacific Northwest. If he's not working with new technologies in web development, you can probably find him out running, climbing, or hiking with his dog.

Leave a Reply