One way to unit test a complex system is to test the behavior of each part and then verify that the structure of the composition is correct.

How do you unit test complex systems? How do you apply TDD to a complex system? This seems to be one of the most difficult questions related to unit testing.

First, a little language lesson. Complex means that something is, by its very nature, composed of parts.

"Complexity is intrinsic. Something is complex if it involves a lot of [metaphorical] moving parts even when considered as a Platonic ideal."
You could say that complexity is the opposite of unity. It's not the same as complicated.
"Complication is extrinsic. Something is complicated by external influences, or because of external influences."
So how do you unit test complex systems?

Traditional introductions to TDD tend to focus on simple systems, such as

  • Stack
  • Fibonacci
  • Prime factors
  • Bowling game
  • Word wrap
Common for these topics is that it's easy to explain the requirements and get started demonstrating how TDD works. It's what John Sonmez calls level 1 unit testing. This is great if the purpose is to introduce the concept of TDD to programmers who are unfamiliar with the TDD process. However, the problem is that it doesn't really explain how to unit test complex systems (level 3 and up).

A partial answer is provided by one of my favorite quotes from Design Patterns:

Favor object composition over class inheritance.
If you can unit test the behavior each sub-component in isolation, then 'all' you need to do is to prove that those parts interact correctly. You can do this by a technique I call Structural Inspection. The idea is to inspect the structure of composed parts - a Facade, if you will. If you know that two composed parts interact in a certain way, and you can also prove that the composed parts have the desired identity, you can prove that a complex system works correctly.

Before I go on, I think it's important to point out that Structural Inspection is one among several approaches you can take to unit testing and TDD. Personally, I've had quite a few successes with this technique, but it tends to require a rather meticulous modus operandi, so it's not suitable in all cases. It works best against complex systems where you can't afford mistakes. The disadvantage is that you'll have to write a lot of tests and at times you may feel that you are moving very slowly, but the advantage is that the final system is not only likely to be correct, but it's proven to be correct.

A note about the following example: it describes a way to unit test a shopping basket, including discounts, VAT, totals, etc. You may think that there are easier ways, and that the technique I describe is overkill. However, I've attempted to strike a balance between something that is sufficiently complex to make the examples realistic, yet still not so complex that it can fit in a single (although long) article.

API design #

Structural Inspection is not only a unit testing technique; it's also an API design philosophy:

What you compose, you can also expose.
This means that if you compose objects by applying the Dependency Inversion Principle, you can expose, as a property or field, what you just injected.

public class Discount : IBasketElement
{
    public Discount(decimal amount)
 
    public decimal Amount { get; }
 
    public IBasketVisitor Accept(IBasketVisitor visitor)
}

In the above example, the Discount class contains an amount (a decimal value), and implements the IBasketElement interface. The Accept method is the only member defined by that interface. The design philosophy of Structural Inspection is very simple: because it accepts an amount in its constructor, it should also expose that value as a public Amount property (or field).

People unused to TDD often react in one of two ways:

  • "I don't like adding members only for testing"
  • "It breaks encapsulation"

Well, if you follow TDD, all production code you write, you write as a response to a test you just wrote. The unit tests are the first client of the production API. The important part is whether or not adding a member breaks encapsulation. Adding a property that exposes something that was passed into the constructor by a client, can hardly be said to break encapsulation. Exposing properties doesn't necessarily break encapsulation. Even if you believe that encapsulation is 'information hiding', it makes no sense to hide data that was passed into the object instance by a third party. The object simply isn't the exclusive owner of that data.

However, since the object (like the Discount class in the above example) already holds an instance of the data as a field, it might as well be courteous and share that information with its client. Otherwise, that would just put the onus on the client to remember the value it passed via the constructor.

Ultimately, adding a property to a concrete class has no impact on the exposed interface. Since a constructor is an implementation detail, by corollary Inspection Properties are also implementation details. While they don't break encapsulation, they are only part of the concrete class. At the interface level, they are invisible. Interfaces are general, but concrete types are specific.

Unit testing #

The API design philosophy is easy to understand, but what does that mean for unit testing? The great thing is that it's possible to test each part of the whole in isolation, and then subsequently prove that parts correctly interact.

Consider the Discount class. It's easy to prove that the injected amount is properly exposed as a property:

[Theory]
[InlineData(1)]
[InlineData(2)]
public void AmountIsCorrect(int expected)
{
    var sut = new Discount(expected);
    var actual = sut.Amount;
    Assert.Equal(expected, actual);
}

Now, that's not particularly interesting in itself, but it's a small part of a larger whole. In the big picture, the concrete class is invisible, but the interfaces it implements are important. In this example, it must implement the IBasketElement interface. That's also easy to prove:

[Fact]
public void SutIsBasketElement()
{
    var sut = new Discount();
    Assert.IsAssignableFrom<IBasketElement>(sut);
}

The above fact only states that Discount implements IBasketElement, but not that it correctly implements it. One last test verifies that this is, indeed, the case:

[Fact]
public void AcceptReturnsCorrectResponse()
{
    var expected = new Mock<IBasketVisitor>().Object;
    var sut = new Discount();
 
    var visitorStub = new Mock<IBasketVisitor>();
    visitorStub.Setup(v => v.Visit(sut)).Returns(expected);
    var actual = sut.Accept(visitorStub.Object);
 
    Assert.Same(expected, actual);
}

This test verifies that the Accept method is correctly implemented. It sets up visitorStub in such a way that it'll only return expected if sut is passed to the Visit method. The actual value returned by the Accept method must be the same as the expected value.

Given these tests, the implementation must look something like this:

public class Discount : IBasketElement
{
    private readonly decimal amount;
 
    public Discount(decimal amount)
    {
        this.amount = amount;
    }
 
    public IBasketVisitor Accept(IBasketVisitor visitor)
    {
        return visitor.Visit(this);
    }
 
    public decimal Amount 
    {
        get { return this.amount; }
    }
}

With these tests in place, you can pretty much forget about the Discount class. It has appropriate concrete behavior and correctly implements the IBasketElement protocol. You can write similar tests for all other classes that implements IBasketElement.

That may still seem trivial, but it begins to become interesting when you consider that a Basket is simply a collection of IBasketElement instances. That enables you to write this (Behavior Verification) test:

[Fact]
public void AcceptReturnsCorrectResult()
{
    // Fixture setup
    var v1 = new Mock<IBasketVisitor>().Object;
    var v2 = new Mock<IBasketVisitor>().Object;
    var v3 = new Mock<IBasketVisitor>().Object;
    var e1Stub = new Mock<IBasketElement>();
    var e2Stub = new Mock<IBasketElement>();
    e1Stub.Setup(e => e.Accept(v1)).Returns(v2);
    e2Stub.Setup(e => e.Accept(v2)).Returns(v3);
 
    var sut = new Basket(e1Stub.Object, e2Stub.Object);
    // Exercise system
    var actual = sut.Accept(v1);
    // Verify outcome
    Assert.Same(v3, actual);
    // Teardown
}

That may look difficult, but it simply states that when the Accept method is invoked with v1, it will pass v1 to the first IBasketElement, and the return value will be v2, which is then passed to the second IBasketElement, which returns v3, which is the last result and thus expected to be returned by the Basket instance itself. This works for all Mock<IBasketVisitor>.Object instances, so according to the Liskov Substitution Principle, it will work for any IBasketVisitor that conforms to the protocol.

In other words: because you know that Discount correctly implements IBasketElement, you are guaranteed that if there's a Discount element in a basket, it will call any visitor's Visit(Discount) method. This puts you in a position to implement an IBasketVisitor to calculate the total for the basket.

A BasketTotalVisitor must correctly implement all Visit methods defined by the IBasketVisitor interface and accumulate a total. However, whenever it encounters a Discount, it must subtract the discount from the total. Because BasketTotalVisitor is only a small part of a complex whole, this is easy to test:

[Theory]
[InlineData(1, 1)]
[InlineData(2, 1)]
[InlineData(3, 2)]
public void VisitDiscountReturnsCorrectResult(
    int initialTotal,
    int discount)
{
    var sut = new BasketTotalVisitor(initialTotal);
 
    var actual = sut.Visit(new Discount(discount));
 
    var btv = Assert.IsAssignableFrom<BasketTotalVisitor>(actual);
    Assert.Equal(initialTotal - discount, btv.Total);
}

Notice how this test uses Structural Inspection by verifying that the returned IBasketVisitor is, indeed, a BasketTotalVisitor instance, which has a Total property that the test verifies matches the expected value. It can only do that because the concrete BasketTotalVisitor class exposes the Total property.

Verifying the whole #

You can keep on working like this: establishing that the behavior of each small part follows the correct protocols. Once all fine-grained building blocks are in place, the only thing left to do is to verify that they are correctly composed. This is where Structural Inspection is very powerful. This is how you can verify that a BasketPipeline will have the correct behavior:

[Fact]
public void SutCorrectlyConvertsToPipe()
{
    CompositePipe<Basket> sut = new BasketPipeline();
 
    var visitors = sut
        .Cast<BasketVisitorPipe>()
        .Select(bvp => bvp.Visitor);
 
    var dv = Assert.IsAssignableFrom<VolumeDiscountVisitor>(visitors.First());
    Assert.Equal(500, dv.Threshold);
    Assert.Equal(.05m, dv.Rate);
 
    var vv = Assert.IsAssignableFrom<VatVisitor>(visitors.ElementAt(1));
    Assert.Equal(.25m, vv.Rate);
 
    var btv = Assert.IsAssignableFrom<BasketTotalVisitor>(visitors.Last());
}

This may look like a verbose test, but it's actually quite information-dense.

First, the BasketPipeline is a CompositePipe<Basket>, and while I haven't shown that part in this article, I know from other unit tests that this class adheres to the Liskov Substitution Principle, so if the test can verify that its content is correct, the composed behavior must also be correct.

In this test case, you can project all the contained pipes into the visitors they adapt. This is only possible because the concrete class BasketVisitorPipe exposes the concrete Visitor property. This is Structural Inspection in action.

The BasketPipeline should be able to apply a volume discount, calculate VAT and the total. Because all the small parts have the correct behavior, you only need to verify that the BasketPipeline has the correct structure.

A business rule states that a volume discount of five percent should be applied if the total value of all basket items is 500 or more. This means that the first visitor should be a VolumeDiscountVisitor, and its threshold should be 500 and the rate .05. You can use Structural Inspection to verify that this is the case.

A legal requirement is that a VAT rate of 25 percent (real Danish VAT rate) should be applied to the value of the basket. This means that the next visitor should be a VatVisitor, and its rate should be .25 .

Finally, the total price for the basket, including discounts and VAT should be calculated. This means that the last visitor should be a BasketTotalVisitor.

The test verifies the structure of the API's Facade, and because the Facade has the correct structure and previous tests have proven that all the parts have the correct behavior, the Facade must have the desired higher-order behavior.

In order to write this article, I TDD'ed all the code presented here (and more), and I deliberately didn't try to apply the BasketPipeline to some Basket instances until I had all the structure in place. Once I had that, I wrote a few Facade Tests as sanity checks, and they all passed in the first attempt.

Conclusion #

Structural Inspection is a useful technique when you need to unit test a complex system, and you need a high degree of confidence that everything works correctly. It's not the only option available to you. Another option is to use Triangulation with a large set of Facade Tests, but there may come a time where the number of test cases required to exercise all possible combinations of business rules becomes so large that Structural Inspection may be a better alternative.

The code I wrote for this article contains 136 test cases, distributed over 88 test methods. It has 100 percent code coverage, and it covers 67 members over 11 classes and 3 interfaces. That's probably too enterprisey for a simple shopping basket calculation that only does volume discounts, VAT and total calculation. However, the way I've modeled this Basket API is quite extensible, so it may be quite well-suited for complex shopping baskets that might include various sorts of inclusive and exclusive discounts, VAT rates that are dependent on the category of goods in the basket, or that may be dependent on the shipping destination. Add to that calculation of shipping rates based on shipping origin and destination, as well as various characteristics (weight, volume, etc.) of the items in the basket, and Structural Inspection starts to look attractive.

Resources #

For more details:


Comments

Jin-Wook Chung #

Thank you for sharing the great article. Recently I have been curious about how the structural instpection can work on funtional programming. Because funtion does not expose any properties to be inspected, I think that the only way to verify that whole thing correctly behaves is doing integration test.

I was wondering whether you had a good approach about it.

2015-02-13 02:12 UTC

Thank you for writing. With functions, I've yet to find a good alternative to Structural Inspection, so I find myself writing more Integration Tests when I write F# code. On the other hand, because the F# type system is so much stronger than e.g. C#, I've also found that I can design data structures in such a way that illegal states become unrepresentable, and that again means that I need to write fewer unit tests than I would have to with F#.

Thus, even though I may have to write slightly more Integration Tests, I'm still quite happy with the trade-off.

2015-02-14 11:07 UTC


Wish to comment?

You can add a comment to this post by sending me a pull request. Alternatively, you can discuss this post on Twitter or somewhere else with a permalink. Ping me with the link, and I may respond.

Published

Thursday, 04 April 2013 07:50:00 UTC

Tags



"Our team wholeheartedly endorses Mark. His expert service provides tremendous value."
Hire me!
Published: Thursday, 04 April 2013 07:50:00 UTC