Friday 25 May 2012

How do I Test-Drive a looped execution?

There are times when we want to specify something happening to multiple items. There is a pattern I'd like to share about specifying such execution. The only precondition is that the loop behavior is just a variant of single "thing" happening to each of the multiple items. Let me clarify this with a little example

Thumbs up: the problem

Let's suppose we're uploading multiple images at once into a social network portal.

Each of the images is processed in the following way:

  • Size of the file is verified and if the verification passes:
    1. A thumbnail is made of the picture
    2. The picture is added to the album along with a thumbnail

If I was to specify the application of this processing to a batch of images in one spec, it would look like this (let's use NSubstitute as a mocking framework, since it's very readable even for guys working with other programming languages than C#):

[Test]
public void 
ShouldAddMultipleImagesWithThumbnailsWhenSizeVerificationSuccessful()
{
  //Given
  var anyImage1 = Substitute.For<Image>();
  var anyImage2 = Substitute.For<Image>();
  var anyImage3 = Substitute.For<Image>();
  var images = new List<Image> { anyImage1, anyImage2, anyImage3 };
 
  var anyThumbnail1 = Substitute.For<Thumbnail>();
  var anyThumbnail2 = Substitute.For<Thumbnail>();
  var anyThumbnail3 = Substitute.For<Thumbnail>();
  anyImage1.CreateThumbnail().Returns(anyThumbnail1);
  anyImage2.CreateThumbnail().Returns(anyThumbnail2);
  anyImage3.CreateThumbnail().Returns(anyThumbnail3);

  var sizeVerification = Substitute.For<SizeVerification>();
  sizeVerification.IsSuccessfulFor(anyImage1).Returns(true);
  sizeVerification.IsSuccessfulFor(anyImage2).Returns(true);
  sizeVerification.IsSuccessfulFor(anyImage3).Returns(true);

  var album = Substitute.For<Album>();
  var imageUpload = new ImageUpload(sizeVerification);

  //When
  imageUpload.PerformFor(images, album);
 
  //Then
  album.Received().Add(anyImage1, anyThumbnail1);
  album.Received().Add(anyImage2, anyThumbnail2);
  album.Received().Add(anyImage3, anyThumbnail3);
}

There is something highly disturbing about this spec. It clearly points that the concept of the processing logic is mixed up with the concept of looping through a collection (this points out that the method is not cohesive, by the way). This gets even most evident when you try to specify a case when size verification does not pass. How would you write such a spec? Make the verification fail for all the items? For one out of three? What about the special case of first one and the last one? How many of these cases is enough to drive the implementation?

The proposed solution

How do I handle such cases? I break apart looping and processing logic by test-driving two public methods - one handling the collection and another handling only one item. The multi-element version of the method is only specified for how it uses the single-element version and the single-element version is specified for the concrete processing logic.

To make this happen, we need to use partial mocks. A Partial Mock is a variant of mock object that allows you to fake only chosen methods from concrete type, leaving the behavior of all other methods as in the original object. NSubstitute does not support partial mocks as of yet, but let's pretend it does by the means of PartialSubstitute class. Here's how our first spec would look like:

[Test]
public void ShouldBePerformedForEachElementOfPassedBatch()
{
  //Given
  var anyImage1 = Any.InstanceOf<Image>();
  var anyImage2 = Any.InstanceOf<Image>();
  var anyImage3 = Any.InstanceOf<Image>();
  var images = new List<Image> { anyImage1, anyImage2, anyImage3 };
  var album = Substitute.For<Album>();
  var imageUpload = PartialSubstitute.For<ImageUpload>(
    Any.InstanceOf<SizeVerification>());

  //mock only the single-element version:
  imageUpload.PerformFor(Arg.Any<Image>(), Arg.Any<Album>()).Overwrite();

  //When
  //invoke the multiple-elements version:
  imageUpload.PerformFor(images, album);
   
  //Then
  imageUpload.Received().PerformFor(anyImage1, album);
  imageUpload.Received().PerformFor(anyImage2, album);
  imageUpload.Received().PerformFor(anyImage3, album);
}

This way we specify looping only, i.e. how the collection of images is handled related to single image handling. Note how the partial mock is verified only for calls made to single-element version by the multi-element version. To make this possible, the single-element version must be virtual:

public void PerformFor(List<Image> images, Album album) {}
public virtual void PerformFor(Image image, Album album) {}

Now that we got rid of the sequence processing, we can proceed with the image processing:

[Test]
public void ShouldAddImageToAlbumWithThumbnail()
{
  //Given
  var anyImage = Substitute.For<Image>();
   
  var anyThumbnail = Substitute.For<Thumbnail>();
  anyImage.CreateThumbnail().Returns(anyThumbnail);

  var sizeVerification = Substitute.For<SizeVerification>();
  sizeVerification.IsSuccessfulFor(anyImage).Returns(true);

  var album = Substitute.For<Album>();
  var imageUpload = new ImageUpload(sizeVerification);

  //When
  imageUpload.PerformFor(anyImage, album);
   
  //Then
  album.Received().Add(anyImage, anyThumbnail);
}

Here, we're dealing with one image only, so the spec is easy to follow and straightforward. Note that if we want to add a spec for image that fails size verification, we add it only for the single-element version. The looping logic is in both cases the same and we've got it specified already.

Another doubt that may come to your mind is this: we're exposing another method in the interface (the single-element version) that's not really used by the clients of the class, so aren't we violating the encapsulation or something? Well, in my opinion, not really. I mean, no one ever forbids you to call the already existing multi-element version with a list consisting of one element, right? So, you may treat this additional method as an alias for this particular case. This way you're not exposing any additional implementation detail that wasn't exposed before.

Ok, that't it! Have a good night!

No comments: