Saturday 7 July 2012

Class mocks - maintaining constructors

In one of my previous posts I promised, that I'd share with you some hints on reducing the cost of maintaining class mocks. Remember that I'm not recommending class mocks, just showing you what to do when you have little choice. That said, I'll try to highlight a way to deal with constructor maintenance.

My plan is as follows: first, I'll introduce the general pattern and describe its canonical use in unit testing. Then I'll show you how adding one teeny tiny method drives us to the solution to our mock constructor issue.

The builder to the rescue!

Before we jump into creating mocks, let's note that there are many unit tests where we create the specified object (e.g. in class X specifications, we create new object of class X in every spec/unit test, at least that's what I do, since I strongly believe that Setup/Teardown are last resort mechanisms).

Now, just to let you get a feeling of what I'm talking about, let's look at simple example:

[Test]
public void ShouldCopyFromSourceToDestination()
{
  //GIVEN
  var source = new Mock<ISource>();
  var destination = new Mock<IDestination>();
  var anyLog = Any.InstanceOf<ILog>();
  var anyFile = Any.InstanceOf<IFile>();  
  var copyOperation = new CopyOperation(source, destination, anyLog);
  source.Setup(m => m.Get()).Returns(anyFile);

  //WHEN
  copyOperation.PerformOn(anyFile);
  
  //THEN
  destination.Verify(m => m.Write(anyFile));
}

Let's see - what does this logging object do here? It's not really part of the behavior - it only makes the intent less visible! Also, when we add another parameter to the constructor, we'll have to update all specs that create a CopyOperation object.

There are cases when having many constructor parameters is a sign that it's high time to introduce a Facade in the design. But sometimes not. In such cases we can use a builder pattern that takes only the parameters that are important. No need for logger when we specify logging-agnostic behaviors. For example:

[Test]
public void ShouldCopyFromSourceToDestination()
{
  //GIVEN
  var source = new Mock<ISource>();
  var destination = new Mock<IDestination>();
  var anyFile = Any.InstanceOf<IFile>();  
  var copyOperation = new CopyOperationBuilder()
    .Source(source)
    .Destination(destination)
    .Build();
  
  source.Setup(m => m.Get()).Returns(anyFile);

  //WHEN
  copyOperation.PerformOn(anyFile);
  
  //THEN
  destination.Verify(m => m.Write(anyFile));
}

The implementation of such builder is dead simple:

public class CopyOperationBuilder()
{
  private ISource _source 
    = new Mock<ISource>().Object;
  private IDestination _destination 
    = new Mock<IDestination>().Object;
  private ILog _log 
    = new Mock<ILog>().Object;

  public CopyOperationBuilder Source(
    ISource source)
  {
    _source = source;
    return this;
  }

  public CopyOperationBuilder Destination(
    IDestination destination)
  {
    _destination = destination;
    return this;
  }

  public CopyOperationBuilder Log(ILog log)
  {
    _log = log;
    return this;
  }

  public CopyOperation Build()
  {
    return new CopyOperation(
      _source, 
      _destination, 
      _log);
  }
}

Easy, isn't it? Not only did we make the specification more meaningful, we also made the spec less prone to changes in constructor - when new parameter is added that doesn't affect the spec, the builder is the only place we need to update.

OK, now that I introduced the pattern and its usual usage in unit testing, let's see how we can benefit from it in case of mocking.

Mock builder

Now, time for a trick. We can just add another method in the builder, that will build a mock for us:

public class CopyOperationBuilder
{
  //all the methods described above...

  public Mock<CopyOperation> BuildMock()
  {
    return new Mock<CopyOperation>(
      _source, 
      _destination, 
      _log) { CallBase = true };
  }
}

And that's it. Simple and sweet trick that may become handy in those very rare cases where we need to create class mocks.

Good night everyone!

No comments: