Breaking static dependency

Static dependency can be nightmare for developers who write tests for their code. There is not much to do to get rid of static dependencies if they come with third-party libraries or NuGet packages. This blog post introduces two tricks to make code with static dependencies testable.

As a sample I take one piece of non-commercial code that has famous static dependency.

public class GetGpsCommand : ICommand<PhotoEditModel>
{
    public bool Execute(PhotoEditModel model)
    {
        var img = ImageFile.FromStream(model.File.OpenReadStream());
        var latObject = (GPSLatitudeLongitude)img.Properties.FirstOrDefault(p => p.Name == "GPSLatitude");
        var lonObject = (GPSLatitudeLongitude)img.Properties.FirstOrDefault(p => p.Name == "GPSLongitude");
 
        if (latObject != null && lonObject != null)
        {
            model.Latitude = latObject.ToFloat();
            model.Longitude = lonObject.ToFloat();
        }
 
        return true;
    }
}

As soon as we want to cover Execute() method with tests we have to run FromStream() method of ImageFile with valid data to avoid exceptions. But then it is not unit test anymore but integration test.

Wrapping static dependency to client class

One option is to use define interface and two implementations – one for tests and one for application.

Breaking static dependency using interface and client classes

This way we can wrap static dependency to client class that is used by application and for this class we use only integration tests.

Let’s start with implementation. Client classes mean that we need also some data structure to return coordinates. Let’s define class GpsCoordiates that is plain Data Transfer Object (DTO). It doesn’t carry any system logic.

public class GpsCoordinates
{
    public float? Latitude { get; set; }
    public float? Longitude { get; set; }
}

We have now DTO to carry coordinates and it’s possible to define interface for client classes.

public interface IImageClient
{
    GpsCoordinates GetCoordinates(Stream stream);
}

First client class we implement is for application. Code is similar as before and we just don’t use model class anymore to assign coordinates This is because we don’t know right now where we later put this code. It’s possible we will move it to some service library that is used by multiple applications in solution,

public class ImageClient : IImageClient
{
    public GpsCoordinates GetCoordinates(Stream stream)
    {
        var img = ImageFile.FromStream(stream);
        var lat = (GPSLatitudeLongitude)img.Properties.FirstOrDefault(p => p.Name == "GPSLatitude");
        var lon = (GPSLatitudeLongitude)img.Properties.FirstOrDefault(p => p.Name == "GPSLongitude");
 
        var result = new GpsCoordinates();
        result.Latitude = lat?.ToFloat();
        result.Longitude = lon?.ToFloat();
 
        return result;
    }
}

For tests we define simple test client that easy to control from unit tests.

public class TestImageClient : IImageClient
{
    public GpsCoordinates CoordinatesToReturn;
    public Exception ExceptionToThrow;
 
    public GpsCoordinates GetCoordinates(Stream stream)
    {
        if(ExceptionToThrow != null)
        {
            throw ExceptionToThrow;
        }
 
        return CoordinatesToReturn;
    }
}

If we want it to throw exception then we can assign some exception to ExceptionToThrow field. If we want it to return coordinates then we can assign value to CoordinatesToReturn field.

Before using these classes we need to modify original class so it gets instance of image client through constructor.

public class GetGpsCommand : ICommand<PhotoEditModel>
{
    private readonly IImageClient _imageClient;
 
    public GetGpsCommand(IImageClient imageClient)
    {
        _imageClient = imageClient;
    }
 
    public bool Execute(PhotoEditModel model)
    {
        var coordinates = _imageClient.GetCoordinates(model.File.OpenReadStream());
            
        if (coordinates != null && coordinates.Latitude != null && coordinates.Longitude != null)
        {
            model.Latitude = coordinates.Latitude;
            model.Longitude = coordinates.Longitude;
        }
 
        return true;
    }
}

Here is one sample test where TestImageClient is used to avoid static dependency.

[Fact]
public void Execute_should_not_assign_if_latitude_is_null()
{
    var coordinates = new GpsCoordinates { Latitude = null, Longitude = 24 };
    var imageClient = new TestImageClient { CoordinatesToReturn = coordinates };
    var model = new PhotoEditModel();
 
    var command = new GetGpsCommand(imageClient);
    command.Execute(model);
 
    Assert.Null(model.Latitude);
    Assert.Null(model.Longitude);
}

If latitude is missing but longitude is present then coordinate properties of model are not assigned.

Using fake class

Another approach is to use fake class and virtual method to return coordinates. It means that in application and integration tests we use method that contains static dependency and in tests we use fake class that overrides coordinates method avoiding static dependency to ImageFile class.

Breaking static dependency using fake class

Let’s start with command class. Here is the code of original command again.

public class GetGpsCommand : ICommand<PhotoEditModel>
{
    public bool Execute(PhotoEditModel model)
    {
        var img = ImageFile.FromStream(model.File.OpenReadStream());
        var latObject = (GPSLatitudeLongitude)img.Properties.FirstOrDefault(p => p.Name == "GPSLatitude");
        var lonObject = (GPSLatitudeLongitude)img.Properties.FirstOrDefault(p => p.Name == "GPSLongitude");
 
        if (latObject != null && lonObject != null)
        {
            model.Latitude = latObject.ToFloat();
            model.Longitude = lonObject.ToFloat();
        }
 
        return true;
    }
}

As said before we need to move static dependency to virtual method so we can override it later. For this we move static dependency to new GetCoordinates() method.

public class GetGpsCommand : ICommand<PhotoEditModel>
{ 
    internal virtual GpsCoordinates GetCoordinates(Stream stream)
    {
        var img = ImageFile.FromStream(stream);
        var lat = (GPSLatitudeLongitude)img.Properties.FirstOrDefault(p => p.Name == "GPSLatitude");
        var lon = (GPSLatitudeLongitude)img.Properties.FirstOrDefault(p => p.Name == "GPSLongitude");
 
        var result = new GpsCoordinates();
        result.Latitude = lat?.ToFloat();
        result.Longitude = lon?.ToFloat();
 
        return result;
    }
 
    public bool Execute(PhotoEditModel model)
    {
        var coordinates = GetCoordinates(model.File.OpenReadStream());
        if (coordinates != null && coordinates.Latitude != null && coordinates.Longitude != null)
        {
            model.Latitude = coordinates.Latitude;
            model.Longitude = coordinates.Longitude;
        }
 
        return true;
    }
}

Now we have static dependency under our control and we can write fake command. GetCoordinates() method of fake command replaces the one in parent class and this way we avoid static dependency in unit tests.

public class FakeGetGpsCommand : GetGpsCommand
{
    public GpsCoordinates CoordinatesToReturn;
    public Exception ExceptionToThrow;
 
    internal override GpsCoordinates GetCoordinates(Stream stream)
    {
        if(ExceptionToThrow != null)
        {
            throw ExceptionToThrow;
        }
 
        return CoordinatesToReturn;
    }
}

Here is example test that uses fake class we created.

[Fact]
public void Execute_should_not_assign_if_latitude_is_null()
{
    var coordinates = new GpsCoordinates { Latitude = null, Longitude = 24 };
    var model = new PhotoEditModel();
    var command = new FakeGetGpsCommand { CoordinatesToReturn = coordinates };
 
    command.Execute(model);
 
    Assert.Null(model.Latitude);
    Assert.Null(model.Longitude);
}

It’s not much longer or complex that test we wrote for client classes approach.

Which one is better – fake class or interface and client classes?

My answer is famous in software development world – it depends! Both approaches have their own pros and cons. It depends heavily on how testable class is built. Based on my own experiences I can bring out some very general pros and cons.

ApproachProsCons
Client class
  • More flexible and extensible
  • Leads to cleaner design
  • Easier to control
  • More new code units
  • Complexity of system grows
  • Easy to use it too much
Fake class
  • Smaller amount of new code units
  • Most changes are in class scope
  • Hard to use with more complex code
  • Complexity grows faster

Wrapping up

Static dependency is horror keyword in unit testing. If it’s our own code then we can change it. If static calls are forced by external library or NuGet package then we actually cannot get away from it. It will always be there and the best thing we can do is to avoid it. This blog post introduced two methods to get rid of static dependency – using interface with client classes and creating fake class that overrides method with static dependency. Both of these methods have their pros and cons. It’s up to developer to decide which approach is better for static dependency in given class to be broken.

Liked this post? Empower your friends by sharing it!

Gunnar Peipman

Gunnar Peipman is ASP.NET, Azure and SharePoint fan, Estonian Microsoft user group leader, blogger, conference speaker, teacher, and tech maniac. Since 2008 he is Microsoft MVP specialized on ASP.NET.

    2 thoughts on “Breaking static dependency

    • February 16, 2019 at 5:18 am
      Permalink

      Your test class TestImageClient has an ‘if’ clause. It is a big NO-NO to have any logic in test code – regardless of the reason for its presence. The reason it is a NO-NO should be obvious: it is logic and thus should be tested. If the logic in that ‘if’ is wrong, it will affect the behavior of the test.

      You have the same problem in your Fakes class.

    • February 24, 2019 at 2:30 pm
      Permalink

      You are exchanging one dependency (the static) with another dependency on the interface IImageClient. Any dependency is a code smell and it should be avoided if possible. In this case the solution is simply to pass the GpsCoordinates as constructor parameters of GetGpsCommand. No more dependencies and very simply to test.

    Leave a Reply

    Your email address will not be published. Required fields are marked *