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:
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:
When I run these test with TestDriven.NET the output in the test window is:
—— Test started: Assembly: InteractionBased.dll ——
Exploring InteractionBased, Version=1.0.2448.18216, Culture=neutral, PublicKeyToken=null
MbUnit 184.108.40.206 Addin
Found 12 tests
TestCase ‘Tests.ProvideSquare.TestInteraction’ failed: Equal assertion failed: []!=[]
Message: Equal assertion failed: []!=[]
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
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.
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.
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.