Breaking Static Dependency: How to Make Code Testable
Learn how you can make code with static dependencies testable.
Join the DZone community and get the full member experience.
Join For Free
Static dependency can be a 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 a 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 the Execute()
method with tests, we have to run theFromStream()
method of the ImageFile with valid data to avoid exceptions. But then, it is not a unit test anymore butan integration test.
Wrapping a Static Dependency to the Client Class
One option is to use the define interface and two implementations – one for tests and one for the application.
This way, we can wrap the static dependency to the client class that is used the by application, and for this class, we only use integration tests.
Let’s start with implementation. Client classes mean that we also need some data structure to return coordinates. Let’s define the class GpsCoordiates
that is a 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; }
}
Now, we have DTO to carry coordinates, and it’s possible to define the interface for client classes.
public interface IImageClient
{
GpsCoordinates GetCoordinates(Stream stream);
}
The first client class that we implement is for the application. The code is similar as before, and we just don’t use the 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 a simple test client that is easy to control from the 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 an exception, then we can assign an exception to the ExceptionToThrow
field. If we want it to return coordinates, then we can assign a value to the CoordinatesToReturn
field.
Before using these classes, we need to modify the original class, so it gets the instance of the image client through the 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 the coordinate properties of the model are not assigned.
Using a Fake Class
Another approach is to use the fake class and virtual method to return coordinates. It means that, in application and integration tests, we use a method that contains static dependency, and in tests, we use a fake class that overrides the coordinates method, avoiding static dependency to the ImageFile
class.
Let’s start with the command class. Here is the code of the 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 the virtual method, so we can override it later. For this, we move static dependency to the 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. The GetCoordinates()
method of the fake command replaces one in the parent class, and this way, we can avoid static dependency in our 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 an example test that uses the 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 more complex than the test we wrote for the client classes approach.
Which One Is Better — Fake Class or Interface and Client Classes?
My answer is very common in the world of software development — it depends! Both approaches have their own pros and cons. It depends heavily on how testable the class is built. Based on my own experiences, I can bring out some very general pros and cons.
Approach | Pros | Cons |
---|---|---|
Client class |
|
|
Fake class |
|
|
Wrapping Up
Static dependency is a horror keyword in unit testing. If it’s our own code, then we can change it. If static calls are forced by the 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 avoid it. This blog post introduced two methods to get rid of static dependency — using an 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.
Published at DZone with permission of Gunnar Peipman, DZone MVB. See the original article here.
Opinions expressed by DZone contributors are their own.
Comments