Unit Test the Behavior, Not the Implementation

As you write tests, you’ll often come across situations where the code which exibits a certain behavior is different than the code which causes the behavior to exist. Consider a simple case in Metsys.Bson – a BSON serializer. You can configure the serializer and deserializer to use a different name for a property by doing something like:

BsonConfiguration.ForType<User>(t => t.UseAlias(u => u.Id, "_id"));

It turns out that this feature was implemented without ever touching the core serializer or deserializer. Both of those rely on the Name property of a neat class called MagicProperty. The logic behind what the name of the property is was encapsulated within MagicProperty. Therefore the serializer and deserializer were able to work as-is, relying on the Name property to be whatever was right.

The point though is that while the code to make this feature work exists largely in the MagicProperty (and the related TypeHelper), I strongly consider this a behavior of the serializer and deserializer. Therefore, the tests to make sure this works are done against those, rather than MagicProperty and TypeHelpe. Here’s what one of those tests looks like:

[Fact]
public void UsesAliasWhenSerializing()
{
    BsonConfiguration.ForType<Skinny>(t =>
    {
        t.UseAlias(p => p.Nint, "id");
        t.UseAlias(p => p.String, "str");
    });

    var result = Serializer.Serialize(new Skinny { Nint = 43, String = "abc" });
    Assert.Equal((byte)'i', result[5]);
    Assert.Equal((byte)'d', result[6 ]);
    Assert.Equal((byte)0, result[7]);

    Assert.Equal((byte)'s', result[13]);
    Assert.Equal((byte)'t', result[14]);
    Assert.Equal((byte)'r', result[15]);
    Assert.Equal((byte)0, result[16]);
}

There isn’t any mention or references to the implementation. When I talk about writing effective unit testing, this is largely what I’m trying to convey. You should mostly focus on testing what your code is doing, not how it’s doing it. It’s also worth mentioning that TDD can really help you shine here – by writing the test first based on what I felt the behavior was, I didn’t run the risk of getting confused by what the implementation turned out to be. Had I done the implementation first, I might have immediately started testing MagicProperty and TypeHelper and considered my job done – without ever writing a test against the intended behavior.

This entry was posted in Uncategorized. Bookmark the permalink. Follow any comments here with the RSS feed for this post.

7 Responses to Unit Test the Behavior, Not the Implementation

  1. I am not sure that you’re testing correctly. Basically in the code I see that you try to test the behavior of one type (MagicProperty) by using another two (Serializer and BsonConfiguration). Thus the test has more dependencies than it should have. What if serialization algorithm changes? That should not be related to MagicProperty behavior, but you’ll need to update all those hard-coded stuff.
    I’d suggest to replace this with a set of tests (checking that Serializer uses Name property to get the name; checking that MagicNumber is being configured by UseAlias(); etc.)

  2. karl says:

    @Arielr.
    Great feedback. I did struggle with how to test that the serializer was properly serializing. You can see that I took a different, less brittle, approach with the deserializer. Maybe I should have taken the same approach with the serializer.

    I agree with you about the ordering – BSON doesn’t specify that field ordering is important. However, the exact location of the end terminator is important – it mustn’t just exist, it must exist and be at the right place. Similarly, the relation between a field type, name, and value must also be guaranteed.

    The purpose of the serializer is to serialize to a specific protocol – BSON; therefore serialization tests will coupled to the BSON implementation….(but I still agree the serialization tests are awkward and brittle).

  3. Arielr says:

    Actually, I think that that test IS testing the implementation and not behavior. You’re bound to the exact location in the serialized string, and the terminator char, and the order of the properties serialized and… well, that’s it.

    Why not use
    {
    result.ShouldContain(“id”)”;
    result.ShouldNotContain(“Nint”)”;
    }

  4. karl says:

    I’m not sure I understand. Maybe you should look at the library, and take a look at the tests :)

  5. Paco says:

    So there has to be a some kind of mapping from POCO to serializable?
    Why not POCO to Bson/Json directly?

  6. karl says:

    because the way the object is represented in your application’s memory will be different than the way its represented in the database. Also, it does have to be transferred from one to the other over a socket.

  7. Paco says:

    Very off topic question: Why should objects stored in an object database be serializable?