September 15, 2006 OOP, Unit Testing

Liskov Substitution Principle and Testability

Well I bet you are wondering how LSP could be related to Testability.  Before we get in to it I want to identify some tar pits that you should avoid; we will touch on some before the end.

  • Composition Over Inheritance
  • Subtype != Subclass

Here an excerpt from c2.com’s page on LSP:

Barbara Liskov wrote LSP in 1988:

What is wanted here is something like the following substitution property: If for each object o1 of type S there is an object o2 of type T such that for all programs P defined in terms of T, the behavior of P is unchanged when o1 is substituted for o2 then S is a subtype of T.” – BarbaraLiskov, Data Abstraction and Hierarchy, SIGPLAN Notices, 23,5 (May, 1988).

See Also – Robert C. Martin, Engineering Notebook, C++ Report, Nov-Dec, 1996. http://www.objectmentor.com/resources/articles/lsp.pdf


When you have an inheritance hierarchy that follows LSP the testing effort for the entire hierarchy can be less than the effort to test the same number of classes not in an inheritance hierarchy.  If verification can be performed through the interface then one test fixture can be used to exercise the hierarchy.  So testability is related to LSP through reuse.  How they are related is important because with out reuse there is no relationship.  Reuse is lost at the same time that LSP is broken (chicken and egg).  Meaning that as soon as a subtype is introduced into the hierarchy that behaves differently than the supertype the benefit to the new subtype is lost and we are in a situation where inheritance is not offering any reuse to increase testability.  I suppose that some reuse could be gained at the expense of maintainability and understandability.  Even when the interface of the supertype does not offer a means for verification reuse of the driver part of the test fixture can easily be achieved while the verification part can vary through composition.  Many languages do not have the features built in to enforce compliance with LSP: unit testing can fill this void, alerting you to violations.

Lets examine an example.  The example includes an interface IShape from which several classes will subtype.  One class, Square, will subclass, from Rectangle.  The test fixture being used is from MbUnit.  The TypeFixture was made for testing type hierarchies, though there are various ways to achieve reuse.

First lets see the test subjects:

 

public interface IShape
{
    int Width { get; set; }
    int Height { get; set; }
}
 
 public class Triangle : IShape
{
    private int _Width;
    private int _Height;
 
    public int Width
    {
        get { return _Width; }
        set { _Width = value; }
    }
 
    public int Height
    {
        get { return _Height; }
        set { _Height = value; }
    }
}
 
 public class Ellipse : IShape
{
    private int _Width;
    private int _Height;
 
    public int Width
    {
        get { return _Width; }
        set { _Width = value; }
    }
 
    public int Height
    {
        get { return _Height; }
        set { _Height = value; }
    }
}
 
 public class Rectangle : IShape
{
    private int _Width;
    private int _Height;
 
    public int Width
    {
        get { return _Width; }
        set { _Width = value; }
    }
 
    public int Height
    {
        get { return _Height; }
        set { _Height = value; }
    }
}
 
 public class Square : IShape
{
    private int _Width;
    private int _Height;
 
    public int Width
    {
        get { return _Width; }
        set 
        { 
            _Width = value;
            _Height = value;
        }
    }
 
    public int Height
    {
        get { return _Height; }
        set 
        { 
            _Height = value; 
            _Width = value;
        }
    }
}

 

I am sure that the first thing you notice is these examples are so simplistic that they are only useful to display the bare principle (don’t worry we will look at a real world example later). Notice that the implementation of Square adds new behavior: setting the width sets that height and vice versa.  Just in case you are not familiar with LSP this is the classic example (much like hello world!).  Here is the unit test fixture I wrote:

 

[TypeFixture(typeof(IShape))]
public class Tests
{
    [Provider(typeof(IShape))]
    public IShape ProvideEllipse()
    {
        return new Ellipse();
    }
    [Provider(typeof(IShape))]
    public IShape ProvideTriangle()
    {
        return new Triangle();
    }
    [Provider(typeof(IShape))]
    public IShape ProvideRectangle()
    {
        return new Rectangle();
    }
    [Provider(typeof(IShape))]
    public IShape ProvideSquare()
    {
        return new Square();
    }
 
    [Test]
    public void TestWidth(IShape testSubject)
    {
        int WidthValue = 2;
        testSubject.Width = WidthValue;
        Assert.AreEqual(WidthValue, testSubject.Width);
    }
 
    [Test]
    public void TestHeight(IShape testSubject)
    {
        int HeightValue = 4;
        testSubject.Height = HeightValue;
        Assert.AreEqual(HeightValue, testSubject.Height);
    }
 
    [Test]
    public void TestInteraction(IShape testSubject)
    {
        int WidthValue = 2;
        int HeightValue = 4;
 
        testSubject.Height = HeightValue;
        testSubject.Width = WidthValue;
 
        Assert.AreEqual(WidthValue, testSubject.Width);
        Assert.AreEqual(HeightValue, testSubject.Height);
    }
}

When I run these test with TestDriven.NET the output in the test window is:

—— Test started: Assembly: InteractionBased.dll ——

Test Execution
Exploring InteractionBased, Version=1.0.2448.18216, Culture=neutral, PublicKeyToken=null
MbUnit 2.3.0.0 Addin
Found 12 tests
[success] Tests.ProvideEllipse.TestWidth
[success] Tests.ProvideEllipse.TestHeight
[success] Tests.ProvideEllipse.TestInteraction
[success] Tests.ProvideTriangle.TestWidth
[success] Tests.ProvideTriangle.TestHeight
[success] Tests.ProvideTriangle.TestInteraction
[success] Tests.ProvideRectangle.TestWidth
[success] Tests.ProvideRectangle.TestHeight
[success] Tests.ProvideRectangle.TestInteraction
[success] Tests.ProvideSquare.TestWidth
[success] Tests.ProvideSquare.TestHeight
[failure] Tests.ProvideSquare.TestInteraction
TestCase ‘Tests.ProvideSquare.TestInteraction’ failed: Equal assertion failed: [[4]]!=[[2]]
MbUnit.Core.Exceptions.NotEqualAssertionException
Message: Equal assertion failed: [[4]]!=[[2]]
Source: MbUnit.Framework
StackTrace:
at MbUnit.Framework.Assert.FailNotEquals(Object expected, Object actual, String format, Object[] args)
at MbUnit.Framework.Assert.AreEqual(Int32 expected, Int32 actual, String message)
at MbUnit.Framework.Assert.AreEqual(Int32 expected, Int32 actual)
c:\projects\unit test patterns\interaction based\lsp.cs(142,0): at Tests.TestInteraction(IShape testSubject)

[reports] generating HTML report
TestResults: file:///c:/projects/unit%20test%20patterns/interaction%20based/bin/debug/InteractionBased.Tests.html

11 passed, 1 failed, 0 skipped, took 1.63 seconds.


So what has this shown us?  The most prominent display was reuse.  It only takes three lines to add a new type to the test suite (I am not counting curly braces as a line).  Because Square deviated from the behavior set by the supertype it failed the unit test TestInteraction: Square violates LSP. STOP!!! do not get cause up in how simple this example is an how it is too weak to fully engage in all the forces at hand in real life.  Lets move into a real life example.

A project I used to work on would log exceptions.  It did so by serializing them, the log viewer would deserialize them.  One part of the system utilized a third party library that would throw exceptions that cause problems in logging (some exceptions would not deserialize).  I used MbUnit’s TestSuiteFixture to create a test fixture that would find all derivatives of System.Exception and test that they are serializable and deserializable.  I used this as an example of how to use the TestSuiteFixture in the article Unit Testing .NET Projects (code example download).  This test fixture was 170 lines, testing 161 subtypes.  It identified 24 subtypes that violate LSP.  This allowed me to account for these violations and fix the logging.  If these types had been under the projects control they could have been corrected to be in compliance with LSP.

Sometimes real life has more complications: a supertype that does not offer a means to validate correctness.  Take IEnumerator for example.  It does not offer a means to test in the same way that IShape or Exception were tested.  The driver portion can be shared but the verification can not be.  In the examples below System.Array is the only test subject shown but first let’s look at the fixture.

 

[TypeFixture(typeof(IEnumerationData))]
public class IEnumeratorTester
{
    [Provider(typeof(IEnumerationData))]
    public IEnumerationData ProvideArrayData()
    {
        return new ArrayData();
    }
 
    [Test]
    [ExpectedException(typeof(InvalidOperationException))]
    public void TestInitialCurrentValue(IEnumerationData testData)
    {
        object InitalValue = null;
        InitalValue = testData.MakeANewTestSubject().Current;
    }
 
    [Test]
    public void TestMovingForward(IEnumerationData testData)
    {
        object Value = null;
        bool HasValue = false;
        IEnumerator TestSubject = testData.MakeANewTestSubject();
 
        HasValue = TestSubject.MoveNext();
        testData.VerifyMoveNextValue(HasValue);
 
        while (HasValue)
        {
            Value = TestSubject.Current;
            testData.VerifyCurrentValue(Value);
 
            HasValue = TestSubject.MoveNext();
            testData.VerifyMoveNextValue(HasValue);
        }
 
        testData.VerifyEndOfMovingForward();
    }
}

Notice how the test fixture only knows of IEnumerator and not of Array.  This fixture could drive any implementation of IEnumerator.  Continued below is the definition of the IEnumerationData interface and the implementation for Array.  Additional implementations can be created for any provider/implementation of IEnumerator and added to the fixture with a new ProviderAttribute decorated method.

public interface IEnumerationData
{
    IEnumerator MakeANewTestSubject();
    void VerifyMoveNextValue(bool actual);
    void VerifyCurrentValue(object actual);
    void VerifyEndOfMovingForward();
}
 
public class ArrayData : IEnumerationData
{
    private String[] _TestSubject;
    public IEnumerator MakeANewTestSubject()
    {
        _TestSubject = new String[3] {"one", "two", "three"};
        return _TestSubject.GetEnumerator();
    }
 
    private bool[] _MoveNextExpectations;
    private bool[] MoveNextExpectations
    {
        get
        {
            if (_MoveNextExpectations == null)
                _MoveNextExpectations = new bool[4] { true, true, true, false };
            return _MoveNextExpectations;
        }
    }
 
    private int _MoveNextCount;
    private int MoveNextCount
    {
        get
        {
            return _MoveNextCount;
        }
        set
        {
            _MoveNextCount = value;
        }
    }
 
    public void VerifyMoveNextValue(bool actual)
    {
        Assert.AreEqual(this.MoveNextExpectations[this.MoveNextCount], actual);
        this.MoveNextCount++;
    }
 
    public void VerifyCurrentValue(object actual)
    {
        Assert.AreEqual(this._TestSubject[this.MoveNextCount - 1], actual);
    }
 
    public void VerifyEndOfMovingForward()
    {
        Assert.AreEqual(this.MoveNextCount, 4, "Expected MoveNext to have been called 4 times");
    }
}

So far we have see simple and complex examples of how unit tests can take advantage of reuse when LSP is adhered to. We have also mentioned using unit tests to locate violators of LSP in third party libraries.  I think it is worth bringing maintainability into the discussion.  If you are in a situation where you think the hierarchy will be large maintainability of LSP may be difficult.  There may be less difficult options.  There may be options that maintain understandability: when LSP is violated understandability and maintainability decrease.  I will try to tread lightly around this tar pit: subclassing.  On c2.com’s LSP page the distinction between subtyping and subclassing is discussed in detail.  If you are subclassing stick to it, do not mix with subtyping.  Meaning when subclassing clients of the subclass should not use polymorphism, they should not know of the superclass.  Personally I think composition should be used in this situation as inheritance is being used to share code not type.

It takes discipline to adhere to LSP.  The test fixture TypeFixture does not lessen the need for discipline.  The use of the TestSuiteFixture and Reflection can reduce the discipline needed.  Such a test fixture would test all types found in the hierarchy alerting you to violations.  The TypeFixure is dependent on a developer adding code to the fixture to even know about a new subtype.  Addressing discipline in other xUnit frameworks can be more complex.  Maybe in a new version of NUnit extensions can be created to help address testing LSP.

9,582 Total Views

2 to “Liskov Substitution Principle and Testability”

  1. Frank Hileman says...

    IShape does not specify any constraints on the properties Width and Height, it is only an interface saying the properties must be implemented. This means LSP is not violated, only that TestInteraction makes invalid assumptions.

    In fact an interface does not specify much of anything. You could argue that good design principles specify that properties must return the value they are set to, and must be independent. But this would not be generally expected of an IShape, since it may have a fixed aspect ratio.

    Convenience properties in a class are typically not serialized to code or binary streams. This means they do not have to be independent, they may perform the task of altering other properties,as a convenience. For example, a Size property may modify both Width and Height. This does not mean convenience should be banned, but rather that simplistic assumptions about property independence must be avoided when creating tests.

  2. игровые автоматы играть says...

    Great remarkable things here. I?¦m very happy to see your post. Thanks so much and i am looking forward to touch you. Will you kindly drop me a mail?

Leave a comment

*

here