My previous post on testing serialization was in-complete.  Since then my tests have evolved enough to warrant another post (at least I think so).

Here's the excerpt from my previous post;

[Test]
public void Should_Serialise_across_service_boundary()
{
    var s = new DataContractSerializer(typeof(BankAccount));
    var account = new BankAccount() { Owner = new Person() };
    s.WriteObject(new MemoryStream(), account);
}

So, what are the shortcomings I'd like to address?

  • The test doesn't assert anything. As long as the serializer doesn't throw an exception, the test passes
  • It isn't particuarly readable and we'll probably end up with a lot of code duplication

However, before addressing the above issues I'd like to explain why we became interested in testing serialisation and how we failed to solve this previously.

Motivation

Unlike my normal tests this is not a TDD practise.  TDD is about driving your design through tests.  This is not my motivation for doing this type of test.

These tests are supporting continuous integration by providing early feedback.

On the project I was designing at the time we'd spent some time tracking down the cause of defects that turned out to be serialization issues.

First attempt

To address this we starting creating a comprehensive set of integration and acceptance tests. However, these took too long to run and we realised things would only get worse as our system grew.

Also, detecting these problems in integration tests increases the feedback loop. This is bad. It would be much better to find these problems sooner - preferably in unit tests.

Unit testing serialization

So let's get back to the tests.

Take the following classes;

public class BankAccount
{
    public string AccountNumber { get; set; }
    public string SortCode { get; set; }
    public Person Owner { get; set;}

public class Person
{
    public string Name { get; set; }
    public DateTime DateOfBirth { get; set; }
}

BankAccount and Person are simple classes, so I'm confident this will serialize correctly.

Now, how about after refactoring this to use interfaces;

public class BankAccount
{
    public string AccountNumber { get; set; }
    public string SortCode { get; set; }
    public IParty Owner { get; set; }
}

public class Person : IParty
{
    public string Name { get; set; }
    public DateTime DateOfBirth { get; protected set; }
}

public interface IParty
{
    string Name { get; set; }
}

This will now fail as the serializer has no knowledge of the interface and the Person's DateOfBirth property has a protected setter.

So here's my test;

[Test]
 public void Should_Serialise_across_service_boundary()
 {
     GivenBankAccount();
     GivenAccountHasBeenSerialised();
     WhenDeserializingFromStream();

     ThenAllPropertiesShouldBeSet();
  }

I like to refactor my test code into methods to reduce noise and increase the intent on the code.  this makes it easier to understand them later. The Givens sets up the scenario I am testing;

private void GivenBankAccount()
{
    account = Builder<BancAccount>.CreateNew().Build();
    person = Builder<Person>.CreateNew().Build();
    account.Owner = person;
}
private void GivenAccountHasBeenSerialised()
{
    storeStream = new MemoryStream();
    serializer = new DataContractSerializer(typeof(BankAccount));
    serializer.WriteObject(storeStream, account);
}

The When..() method deserializes the stream back into a BankAccount instance;

private void WhenDeserializingFromStream()
{
    storeStream.Position = 0;
    savedAccountDetails = this.serializer.ReadObject(storeStream) as BankAccount;
}

And finally, we test the deserialized object's properties are the same as the original;

private void ThenAllPropertiesShouldBeSet()
{
    savedAccountDetails.ShouldNotBe(null);
    savedAccountDetails.SortCode.ShouldBe(account.SortCode);
    savedAccountDetails.AccountNumber.ShouldBe(account.AccountNumber);
    savedAccountDetails.Owner.Name.ShouldBe(person.Name
    ((Person)savedAccountDetails.Owner).DateOfBirth.ShouldBe(person.DateOfBirth);

}

This test will pick up both problems; the DateOfBirth property won't be set correctly and the serializer will prompt to add the [KnownType(typeof(Person))] attribute to the BankAccount class;

To aid re-use and reduce code clutter I've created some extension methods to serialize and de-serialize objects;

public static class SerialisationExtensions
{
    public static Stream Serialize(this T target)
    {
        Stream storeStream = new MemoryStream();
        var serializer = new DataContractSerializer(typeof(T));
        serializer.WriteObject(storeStream, target);
        return storeStream;
    }

    public static T DeSerialize<T>(this Stream fromStream) where T: class
    {
        fromStream.Position = 0;
        var serializer = new DataContractSerializer(typeof(T));
        return serializer.ReadObject(fromStream) as T;
    }
}

This reduces code duplication in my tests. The below methods;

private void GivenAccountHasBeenSerialised()
{
    storeStream = new MemoryStream();
    serializer = new DataContractSerializer(typeof(BankAccount));
    serializer.WriteObject(storeStream, account);
}

private void WhenDeserializingFromStream()
{
    storeStream.Position = 0;
    savedAccountDetails = this.serializer.ReadObject(storeStream) as BankAccount;
}

are replaced with these;

private void GivenAccountHasBeenSerialised()
{
   storeStream = account.Serialize();
}

private void WhenDeserializingFromStream()
{
    savedAccountDetails = storeStream.DeSerialize<BancAccount>();
}

This makes these methods redundant and we could move this code back into the test.

What we learnt

Creating a comprehensive suite of integration and acceptance tests did prevent our team deploying broken software. However, these were slow running tests and slowed us down.

Finding a way of moving these tests from integration to unit tests enabled us to be faster by reducing our feedback loop.

I've still got some more work to do on testing serialization but this method is easier to understand and more reliable than testing that attributes exist on your classes.