In my previous post about XPathNavigator, I explained in what circumstances the default implementation of XPathNavigator is troublesome. I went over the design of the class and highlighted how that design helps us re-implement XPathNavigator to address the issue.
Testing XPathNavigator
First things first, before attacking the new implementation proper, we want to make sure our implementation is compatible with the default implementation. To do so, we will write tests that will be run both against the Microsoft implementation as well as our implementation once it exists. Our goal here is really twofold. On the one hand, we want to ensure the existing implementation actually works as documented. On the other hand, we want to check our own implementation against the specification tests.
What should we test ?
XPathNavigator is a complex class. So we want to limit my tests to what actually matters for the new implementation. Otherwise, we may be writing literally hundreds of tests.
It is obviously not necessary to test methods that will not be re-implemented. In the previous post, we identified a subset of methods that we will need to re-implement. All other methods are somehow using this basic subset to implement their functionality. The subset is the list of abstract members:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
public abstract string BaseURI { get; } public abstract bool IsEmptyElement { get; } public abstract string LocalName { get; } public abstract string Name { get; } public abstract string NamespaceURI { get; } public abstract XmlNameTable NameTable { get; } public abstract XPathNodeType NodeType { get; } public abstract string Prefix { get; } public abstract bool MoveTo(XPathNavigator other); public abstract bool MoveToFirstAttribute(); public abstract bool MoveToFirstChild(); public abstract bool MoveToFirstNamespace(XPathNamespaceScope namespaceScope); public abstract bool MoveToId(string id); public abstract bool MoveToNext(); public abstract bool MoveToNextAttribute(); public abstract bool MoveToNextNamespace(XPathNamespaceScope namespaceScope); public abstract bool MoveToParent(); public abstract bool MoveToPrevious(); |
As you can see, we have two distinct groups:
- The abstract properties expose information about the current node. Our tests will ensure that we get consistent information for all types of node.
- The abstract methods are all concerned about moving the navigator to another node. The tests need to check that the move operations result in the navigator pointing to the right node given a known starting position.
How should we test it ?
We will test the properties by setting up a XPathNavigator that points to specific nodes of an xml document. Once setup, we simply check the properties expose consistent values. We will test the Move() operations in a very similar way. We will setup the XPathNavigator instance on a specific node, execute the Move() operation we want to test and then check that the XPathNavigator yields values through its properties that are consistent with the navigator’s new position.
This is actually very similar. The only difference is the Move() operation. The similarity will let us factor our most of the test code into a few utility functions.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
private void CanMoveImpl(MoveTestArgs args, Func<XPathNavigator, bool> moveOperation) { CheckInconclusive(args); // Arrange - get a navigator on requested node var nav = CreateNavigatorOnSelected(args.Xml, args.InitialPosition); // Act - move thenode var success = moveOperation(nav); // Assert -- check if success consistent with ShouldSucceed Expect(success, args.ShouldSucceed ? (Constraint)True : False, "inconsistent success state"); // Assert -- check node properties ExpectNodeProperties(nav, args); } |
CanMoveImpl() acts as a parametrized test. It takes 2 arguments:
- args: a MoveTestArgs instance. This argument describes the test’s original state and the resulting state we should test against.
- moveOperation: A delegate to the Move() operation to test. Passing the operation to test as a parameter let us also write non-Move() tests by simply passing a no-op callback.
NUnit: I am using NUnit to write the unit tests. It is only a matter of preference. You can adapt the tests to work against another testing framework such as Microsoft Unit Testing Framework. I find NUnit to be simple to use, non-obstrusive and very flexible.
CanMoveImpl() is called by actual test methods like the following:
1 2 3 4 |
[TestCaseSource("CanMoveToNext_Source")] public void CanMoveToNext(MoveTestArgs args) { CanMoveImpl(args, n => n.MoveToNext()); } |
It is a parametrized test. The TestCaseSource attribute tells NUnit which method to call to get the MoveTestArgs instance for each test.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 |
public IEnumerable<MoveTestArgs> CanMoveToNext_Source() { yield return new MoveTestArgs() { Xml = @"<root></root>", InitialPosition = "/", // selects root ShouldSucceed = false }; yield return new MoveTestArgs() { Xml = @"<root><child/></root>", InitialPosition = "/root/child", ShouldSucceed = false }; yield return new MoveTestArgs() { Xml = @"<root><child/><child2/></root>", InitialPosition = "/root/child", NodeType = XPathNodeType.Element, LocalName = "child2", }; /* ... */ } |
Method CanmoveToNext_Source() returns each test case for a given operation. In the above example, we have the test cases for “when position on document root, MoveToNext() should fail”, “When positioned on element whith no next sibling, MoveToNext() should fail” and “when positioned on an element with a next sibling, MoveToNext() should succeed and point to the specific node”.
Each test case is defined by specifying values for the fields of class CanMoveArgs.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 |
public class MoveTestArgs { // Xml document public string Xml; // XPath to select starting first position public string InitialPosition; // value to test against - no assertion for a given property when not set public string BaseURI; public bool? IsEmptyElement; public string LocalName; public string Name; public string NamespaceURI; public string NameTable; public XPathNodeType? NodeType; public string Prefix; public string Value; // Indicates whether the move should succeed or not public bool ShouldSucceed = true; // indicates whether the test is inconclusive public string Inconclusive; // TestCaseSource calls ToString() on each test case argument to create the test case name. public override string ToString() { if (ShouldSucceed) return string.Format("{0} -- {1} -- {2} -- {3}", Xml, InitialPosition, NodeType, LocalName); else return string.Format("{0} -- {1} -- fails", Xml, InitialPosition); } } |
Method ExpectNodeProperties() implements the assertions depending on the configuration of its MoveTestArgs instance:
1 2 3 4 5 6 7 8 |
private void ExpectNodeProperties(XPathNavigator navigator, MoveTestArgs args) { if (args.LocalName != null) Expect(navigator.LocalName, EqualTo(args.LocalName), "bad localname"); if (args.Name != null) Expect(navigator.Name, EqualTo(args.Name), "bad name"); if (args.Prefix != null) Expect(navigator.Prefix, EqualTo(args.Prefix), "bad prefix"); if (args.NamespaceURI != null) Expect(navigator.NamespaceURI, EqualTo(args.NamespaceURI), "bad namespace uri"); if (args.NodeType != null) Expect(navigator.NodeType, EqualTo(args.NodeType), "bad node type"); if (args.Value != null) Expect(navigator.Value, EqualTo(args.Value), "bad value"); } |
Executing our tests
We want our tests to be executed against the Microsoft implementation as well as our own implementation. The most straight-forward way of achieving this is to implement our tests in an abstract test fixture. The abstract fixture has an factory method to create an instance of XPathNavigator to test against. For each implementation, we create a subclass of our fixture and override the factory method.
CreateNavigable returns an IXPathNavigable. In turn IXpathNavigable lets us create a navigator positioned on the document root thanks to its CreateNavigator() method.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
[TestFixture] public abstract class XPathDocumentTests : AssertionHelper { protected abstract IXPathNavigable CreateNavigable(string xml); /* tests implementations */ } public class MsXPathDocumentTests : XPathDocumentTests { protected override IXPathNavigable CreateNavigable(string xml) { TextReader textReader = new StringReader(xml); XmlReaderSettings settings = new XmlReaderSettings(); settings.IgnoreWhitespace = false; XmlReader reader = XmlReader.Create(textReader, settings); return new XPathDocument(reader, XmlSpace.Preserve); } } |
We’ll add the test fixture for our own implementation when we have the skeleton available. In the mean time, this lets us verify our expectations against the actual implementation of XPathNavigator.
The next post on the topic will tackle the new implementation’s design. I’ll make the implementation and test available as a source code download at the end of this series of articles.