It's time to do the moral thing and try some test-driven XSLT. Our purpose is to get set up to look up the chapter number in our book chapters. The story is something like "Use the chapter number stored in the book table of contents to put the chapter number in the chapter." We've done the manual experiment in the prvious chapter, typing in the example from XSLT Quickly and making it work. We'll try to replicate that experiment in test form, for posterity, and then move on to the actual chapter numbering lookup.
I'm starting with a new blank project in Visual Studio, named XSLTexperiments. I'll begin by adding a test fixture, TestXSLT.cs. I'm looking at an example in "Professional C# 2nd Edition", so I know I need a few using statements. The starting file looks like this:
using System;
using System.IO;
using System.Xml.Xsl;
using System.Xml.XPath;
using NUnit.Framework;
namespace XSLTexperiments {
[TestFixture] public class TestXSLT: Assertion {
public TestXSLT() {
}
[SetUp] public void SetUp() {
}
[Test] public void TESTNAME() {
}
}
}
My plan is to use strings in the program to define the XML to be translated and the XSLT transform. The book example uses files, but I'm thinking that putting the strings directly in the tests will be better. I define two methods to answer the strings, and a test. The test looks like this:
[Test] public void SimpleTransform() {
StringReader rdr = new StringReader(SimpleInput());
XPathDocument doc = new XPathDocument(rdr);
XslTransform transform = new XslTransform();
StringReader xslStringReader = new StringReader(SimpleXSL());
XmlTextReader xslReader = new XmlTextReader(xslStringReader);
transform.Load(xslReader);
StringWriter w = new StringWriter();
XPathNavigator nav = doc.CreateNavigator();
transform.Transform(nav, null, w);
AssertEquals(ResultString(), w.ToString());
}
We "just" create an XPathDocument on the SimpleInput string, create a Transform and load the SimpleXSL into it, create an XPathNavigator on the document, and transform it. We compare the result to our expected result. (Naturally, I created the expected result by inspecting the output by eyeball, and then copying it.) The string methods look like this:
private String SimpleInput() {
String s =
@"<?xml version=""1.0""?>
<title>
This is the title.
</title>";
return s;
}
private String SimpleXSL() {
String x =
@"<xsl:stylesheet version=""1.0"" xmlns:xsl=""http://www.w3.org/1999/XSL/Transform"">
<xsl:template match=""title"">
HELLO
<xsl:apply-templates/>
</xsl:template>
<xsl:template match=""*|@*|comment()|processing-instruction()|text()"">
<xsl:copy>
<xsl:apply-templates select=""*|@*|comment()|processing-instruction()|text()""/>
</xsl:copy>
</xsl:template>
</xsl:stylesheet>";
return x;
}
private String ResultString() {
String r =
@"<?xml version=""1.0"" encoding=""utf-16""?>
HELLO
This is the title.
";
return r;
}
This test runs, but the inputs are too busy to really communicate well. Because our purpose here is to document how to use XSLT, let's see if we can simplify things a bit. I think I'll remove the contents of the "title" tag, and change the match just to report that it has matched title by putting text into the output. Let's try this:
private String SimpleInput() {
String s =
@"<?xml version=""1.0""?>
<title>
</title>";
return s;
}
private String SimpleXSL() {
String x =
@"<xsl:stylesheet version=""1.0""
xmlns:xsl=""http://www.w3.org/1999/XSL/Transform"">
<xsl:template match=""title"">
Title Seen
</xsl:template>
</xsl:stylesheet>";
return x;
}
private String ResultString() {
String r =
@"<?xml version=""1.0"" encoding=""utf-16""?>
Title Seen
";
return r;
}
[Test] public void SimpleTransform() {
StringReader rdr = new StringReader(SimpleInput());
XPathDocument doc = new XPathDocument(rdr);
XslTransform transform = new XslTransform();
StringReader xslStringReader = new StringReader(SimpleXSL());
XmlTextReader xslReader = new XmlTextReader(xslStringReader);
transform.Load(xslReader);
StringWriter w = new StringWriter();
XPathNavigator nav = doc.CreateNavigator();
transform.Transform(nav, null, w);
AssertEquals(ResultString(), w.ToString());
}
Truth be told, this is almost as simple as it gets -- at least as simple as I can make it if the strings are to be kept in the code. It turns out that reading the XML and XSLT from files may actually be simpler -- which is good, since it's a much more common situation. Let's write that same test to run from files that I'll create with the same contents as the input strings. It looks like this:
[Test] public void SimpleFileTransform() {
XPathDocument doc = new XPathDocument("..\\..\\simpleinput.xml");
XslTransform transform = new XslTransform();
transform.Load("..\\..\\simplexsl.xsl");
XPathNavigator nav = doc.CreateNavigator();
StringWriter w = new StringWriter();
transform.Transform(nav, null, w);
AssertEquals(ResultString(), w.ToString());
}
This test runs as well. But I'm not happy.
Our purpose here is to record our learning about XSLT in these tests. For that to happen the code has to help us communicate, and I don't think it is. The weird strings are part of it, but mostly I think it is the multi-step setup of the transform that is obscuring what's going on. Let's see what we can do do clean it up.
Looking at the two test methods, we see a common shape. They create a document, make a navigator out of the document, create a transform, load it, and run it. They both put output on a StringWriter, and the test really only cares about the string. Let's try to remove some of that duplication. First, let's create the transform as a member variable, in SetUp. That removes one line from each test, like this:
[TestFixture] public class TestXSLT: Assertion {
private XslTransform transform;
[SetUp] public void SetUp() {
transform = new XslTransform();
}
[Test] public void SimpleTransform() {
StringReader rdr = new StringReader(SimpleInput());
XPathDocument doc = new XPathDocument(rdr);
StringReader xslStringReader = new StringReader(SimpleXSL());
XmlTextReader xslReader = new XmlTextReader(xslStringReader);
transform.Load(xslReader);
StringWriter w = new StringWriter();
XPathNavigator nav = doc.CreateNavigator();
transform.Transform(nav, null, w);
AssertEquals(ResultString(), w.ToString());
}
[Test] public void SimpleFileTransform() {
XPathDocument doc = new XPathDocument("..\\..\\simpleinput.xml");
transform.Load("..\\..\\simplexsl.xsl");
XPathNavigator nav = doc.CreateNavigator();
StringWriter w = new StringWriter();
transform.Transform(nav, null, w);
AssertEquals(ResultString(), w.ToString());
}
Not much improvement yet, but maybe a little. I'm still looking for duplication. Notice that stuff about creating a new StringWriter, transforming into it, and sending it ToString(). Let's extract that into a method that returns the string:
[Test] public void SimpleTransform() {
StringReader rdr = new StringReader(SimpleInput());
XPathDocument doc = new XPathDocument(rdr);
StringReader xslStringReader = new StringReader(SimpleXSL());
XmlTextReader xslReader = new XmlTextReader(xslStringReader);
transform.Load(xslReader);
XPathNavigator nav = doc.CreateNavigator();
String result = Transform(nav);
AssertEquals(ResultString(), result);
}
[Test] public void SimpleFileTransform() {
XPathDocument doc = new XPathDocument("..\\..\\simpleinput.xml");
transform.Load("..\\..\\simplexsl.xsl");
XPathNavigator nav = doc.CreateNavigator();
String result = Transform(nav);
AssertEquals(ResultString(), result);
}
private String Transform(XPathNavigator nav) {
StringWriter w = new StringWriter();
transform.Transform(nav, null, w);
return w.ToString();
}
That's a bit better. Now I don't like the name of the ResultString() method, however. It's the expected string. We'll rename it to ExpectedString(). Done, tested, works. I'll spare you another listing with just that word changed. Watch for it next time. What else is duplicated?
Well, there's the creation of the document which is then wrapped in a navigator. We aren't looking at the document at all. Let's see if we can remove that. One way would be to write a new private method that returns the navigator, then use it in our Transform() method. It is tempting to make the navigator into a member variable, but I'm going to hold off on that for two reasons. First, it would be a bigger step than I'd like to take. I'd have to extract the code, plus change all the Transform() calls. Second, since the only use of the navigator is inside the Transform method, it doesn't have quite the same right to life as the transform does. The two member variables would have different lifetimes. I'm not sure I'm comfortable with that. So I'll write the method to return a navigator, at least for now. The navigator is created from a document ... hmmm. I'm not sure this will be worth it. First, let's reorder the two test methods, and add some whitespace, to better show the commonality:
[Test] public void SimpleTransform() {
StringReader rdr = new StringReader(SimpleInput());
XPathDocument doc = new XPathDocument(rdr);
XPathNavigator nav = doc.CreateNavigator();
StringReader xslStringReader = new StringReader(SimpleXSL());
XmlTextReader xslReader = new XmlTextReader(xslStringReader);
transform.Load(xslReader);
String result = Transform(nav);
AssertEquals(ExpectedString(), result);
}
[Test] public void SimpleFileTransform() {
XPathDocument doc = new XPathDocument("..\\..\\simpleinput.xml");
XPathNavigator nav = doc.CreateNavigator();
transform.Load("..\\..\\simplexsl.xsl");
String result = Transform(nav);
AssertEquals(ExpectedString(), result);
}
I don't see how to do much good removing that duplication. If there were two tests running from files, or two tests running from literal strings, it might be more clear what to do. Let's turn our attention elsewhere.
Neither of these tests is communicating really well yet. I have a preference for the one where the XML and XSLT are right in the source code, though. It's a little easier to keep track of what is supposed to be happening than when we have to browse two additional files just to see what is going on. But there are things I don't like about those as well. First of all, looking at the test itself, we don't see what the inputs are: we have to look at the other methods, and in this case that is bugging me. Second, I don't like the look of the strings in the code. Here's an example:
private String SimpleXSL() {
String x =
@"<xsl:stylesheet version=""1.0""
xmlns:xsl=""http://www.w3.org/1999/XSL/Transform"">
<xsl:template match=""title"">
Title Seen
</xsl:template>
</xsl:stylesheet>";
return x;
}
That's about as simple an example as we'll ever have and it's not easy to figure out. I find those doubled quotes hard to read, and the stylesheet line at the top is really ugly. So the files are hard to find but a bit easier to read, while the literals are easy to find but kind of ugly to read. It's a puzzlement. What are some options?
One good option is to get over it and live with the situation for now. We probably won't need many of these tests, so maybe this is good enough. As we do more tests, maybe we'll get a better idea.
A more exotic option would be to set up each test in its own file. What if we had a single file for each test, looking kind of like our acceptance tests? Something like this:
*XML <?xml version="1.0" ?> <title> </title> *XSL <xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform"> <xsl:template match="title"> Title Seen </xsl:template> </xsl:stylesheet> *EXPECTED <?xml version="1.0" encoding="utf-16"?> Title Seen
Well, that probably would be more clear. Is it worth it for what we're doing right now? I'm not sure that it is. It's important to remember that we're working on a user story about getting the chapter number into the chapters. And we're trying to document our learning a little bit, by writing some tests. So let's get back to learning how to do the job, and then back to doing the job. We'll write a new test that replicates what we learned in the manual test in the previous chapter, but we'll automate it
For the lookup, we're going to have to have at least some of the input in a file. The lookup table is in a separate file from the main XSLT, and since that's part of the real problem, it's OK that our test will have to work that way. I'll set up a some simple test files. First, a little XML that looks like a chapter:
<?xml version="1.0" encoding="utf-8" ?>
<page>
<header>
<chapnum/>
<title>Title</title>
<author>Ron</author>
</header>
<P>This is some paragraph.</P>
</page>
My plan with <chapnum> is to write a tag in the XSLT that uses the lookup to put the chapter number in, wherever the chapnum tag shows up. Probably some of these names could be the same, but because I don't know just how to do this, I'm keeping them all different for now. And, frankly, having trouble thinking of good ones. We'll see what we get and try to clean it up when it works.
Then a chapter info file, that looks a bit like our table of contents does now:
<?xml version="1.0"?>
<page>
<chapter name="Something">
<sectionnumber>0</sectionnumber>
<chapternumber>00.0</chapternumber>
<title>Something</title>
</chapter>
<chapter name="Title">
<sectionnumber>0</sectionnumber>
<chapternumber>05.0</chapternumber>
<title>Title</title>
</chapter>
</page>
What I'm hoping to do here is to use the name attribute in the chapter tag to find the right chapter, then pull the chapternumber out somehow. Looking forward, I'm planning to give each chapter a short unique chapter name; not the real title, but one that won't change. We'll see how that works out.
Finally, some XSLT that looks like what we had in the previous example, showing how we plan to insert the chapter number:
<?xml version="1.0" encoding="utf-8" ?>
<xsl:stylesheet version="1.0"
xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
<xsl:variable name="chapterLookupDoc" select="document('TOC.xml')"/>
<xsl:key name="titleName" match="chapter" use="@name"/>
<xsl:template match="chapter">
</xsl:template>
<xsl:template match="chapnum">
<xsl:apply-templates select="$chapterLookupDoc"/>
<xsl:variable name="bookTitle" select="title"/>
<xsl:for-each select="$chapterLookupDoc">
<xsl:value-of select="key('titleName', $bookTitle)"/>
</xsl:for-each>
</xsl:template>
</xsl:stylesheet>
This is a lot of working blind. Got to write a test and see why it blows up. I feel sure that it will, but I can't think of a smaller bite. Here's the test:
[Test] public void SimpleLookupTransform() {
XPathDocument doc = new XPathDocument("..\\..\\chapter.xml");
XPathNavigator nav = doc.CreateNavigator();
transform.Load("..\\..\\Chapter.xsl");
String result = Transform(nav);
AssertEquals("", result);
}
This gets an error, of course. The output, in addition to the XML header, is "TitleRonThis is some paragraph." That's all the text from the oridinal document. I'm not sure whether it all came out of the value-of, or what. Got to put in some bracketing information in the XSLT. Let's try this:.
<?xml version="1.0" encoding="utf-8" ?>
<xsl:stylesheet version="1.0"
xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
<xsl:variable name="chapterLookupDoc" select="document('TOC.xml')"/>
<xsl:key name="titleName" match="chapter" use="@name"/>
<xsl:template match="chapter">
</xsl:template>
<xsl:template match="chapnum">
<xsl:apply-templates select="$chapterLookupDoc"/>
<xsl:variable name="bookTitle" select="title"/>
<xsl:for-each select="$chapterLookupDoc">
[[[<xsl:value-of select="key('titleName', $bookTitle)"/>]]]
</xsl:for-each>
</xsl:template>
</xsl:stylesheet>
The only difference is the square brackets around the value-of. If everything comes out inside those, then we know the issue is tuning the value-of. If some of the text is coming out elsewhere, we'll have to figure out why.
Gack. Worst possible case! Nothing inside the brackets. Everything outside them. I'm not clear on why the text all printed. We don't have that default selection in there that I thought was necessary. Maybe Microsoft's XSLT handler defaults differently, or maybe I'm confused. Let's add some more debugging:
<xsl:template match="chapnum">
CHAPNUM(
<xsl:apply-templates select="$chapterLookupDoc"/>
<xsl:variable name="bookTitle" select="title"/>
((<xsl:value-of select="$bookTitle"/>))
<xsl:for-each select="$chapterLookupDoc">
[[[<xsl:value-of select="key('titleName', 'Title')"/>]]]
</xsl:for-each>
)CHAPNUM
</xsl:template>
I put "CHAPNUM" around the whole template, to be sure that it's running. This was basically a panic move, as the fact that the square brackets are coming out tells me that. Notice that I changed the last value-of select to key on "Title", not on $bookTitle. I'm thinking that the xsl:variable isn't settin it right. Here's the output, with some whitespace removed:
CHAPNUM(
(())
[[[
0
05.0
Title
]]]
)CHAPNUM
TitleRonThis is some paragraph.
Ha! The full contents of the chapter tag are coming out, because we keyed directly on "Title" rather than on $bookTitle. And notice the empty parens. The xsl:variable isn't working. Why not? Here's just that statement and what it's supposed to be matching:
<xsl:variable name="bookTitle" select="title"/>
<header>
<chapnum title="Title"/>
<title>Title</title>
<author>Ron</author>
</header>
See the bug? The select should say @title ... the ATtribute in the chapnum tag! We'll fix that:
CHAPNUM(
((Title))
[[[
0
05.0
Title
]]]
)CHAPNUM
We're using the $bookTitle variable again, and getting the contents of the chapter tag. Remember, that looks like this:
<chapter name="Title">
<sectionnumber>0</sectionnumber>
<chapternumber>05.0</chapternumber>
<title>Title</title>
</chapter>
We're selecting out all the text, the "0", "05.0", and "Title". That's not exactly what we want, but at last we've made the connection! I'll remove the debugging chapnum and parens (but leave the square brackets. Then it's time to assess where we are.
Well, the good news is that I've kind of got this down to a science. I just edit the XML and XSL files in Visual Studio, save them, then use NUnit to run the test. I look at the console, which is printing the output from the transform. This is at least easy. But it doesn't feel like Test-Driven Development.
In TDD, we proceed very incrementally. We write a very simple test, make it pass, write another, make it pass, and so on. After we have enough tests passing, we use the existence of all the tests to support us while we clean up the code. This isn't like that. We've got tests, and they are documenting where we wind up, and they are helping us discover what happens with this XSLT code, but I don't feel that they're helping us progress, "driving" the result. It's more like we're just changing the code and looking at the output to see if we like it better. We're debugging the program into existence. That's the old-fashioned way. How can we do better?
Well, part of the problem is that we are only testing by looking at the complete contents of the output. In a chat with Chet -- sort of a phone-pairing session -- he reminded me of a report program test script that he wrote. The script could look for lines in the report with first this substring, then that. And so on. Maybe we could do something like that.
You're probably wondering why we're going to all this trouble. Certainly I am. My thoughts are these. First, if we can get some decent tests for this stuff, it really will help document how it works. That should help me later, and in a bigger project it would almost certainly help the maintenance people. Second, I'm thinking that we're going to learn something about how to test this kind of solution. Remember that early in the book, I was writing some rather procedural code, and wasn't sure how to write tests for it. Later, I got better at testing, as I became more familiar with the tools and the language. And things began to go better. I'm hoping that the same will happen here. Let's try it.
The output that's coming out of our test right now includes the chapter number, but also the section number and the title. The output looks like this:
[[[
0
05.0
Title
]]]
(There's also the fact that the contents of the article are coming out and I didn't expect that, but I'm not worried about that now. I'd like for just the chapter number, "05.0" to come out. So I think I'll write a test for "[[[05.0]]]". That should ensure that things are sorted out. First I'll change our existing test to look for that, since it's not really working yet anyway:
[Test] public void SimpleLookupTransform() {
XPathDocument doc = new XPathDocument("..\\..\\chapter.xml");
XPathNavigator nav = doc.CreateNavigator();
transform.Load("..\\..\\Chapter.xsl");
String result = Transform(nav);
int begin = result.IndexOf("[[[");
int end = result.IndexOf("]]]");
String found = result.Substring(begin,end - begin + 3);
AssertEquals("[[[05.0]]]", found);
}
Now I was thinking ahead a little here. By getting the indices of the surrounding square brackets and pulling out the entire substring, my AssertEquals, when it fails, will give a message that shows what's really there instead of what I want. That will help us figure out how make the code better. Naturally this test fails: it reports the entire string shown above inside the square brackets: the "0", the "05.0", and "Title", with newlines between them all. That's OK, we expected that. Now to make it work better. I anticipate that we may have to change the XSLT, but we may also have to change the format of the table of contents to make things easier. That's OK -- we're doing incremental iterative development here and it's natural that we are approximating our way to what we want.
Now I'm distinguishing a subtle difference here. We started this test because I felt I was "debugging" my way to a solution, and now I'm talking about "approximating" our way. Is there a difference? If so, what is it?
When I feel that I'm "debugging" my way to a solution, it's one of two cases. Either I have written code that I think should work, but it doesn't, and I'm changing it more or less at random until it works. Or I don't even know what code to write and I'm sort of randomly writing things to see what happens. Now that's a valid state to be in, for a while. If we don't know how something works, we play with it, poke on it, to see what it does. But when I'm trying to accomplish a particular thing, and when it feels like I'm still poking and playing, I see that as an indication that I'm thrashing.
When I'm thrahing, I want to get more directed. I want to focus more sharply on what I'm trying to do, and I want to make small changes that are supposed to take me in the right direction. To me, it's a subtle difference, but being sensitive to it is what keeps mre from spending my life in the debugger, and keeps me more focused on doing things that are progressing.
In any case, looking at this test, we see what is happening and what we wish would happen. The lookup block that we're using contains more information than we want. It looks like this:
<chapter name="Title">
<sectionnumber>0</sectionnumber>
<chapternumber>05.0</chapternumber>
<title>Title</title>
</chapter>
All three of the section number, chapter number, and title are in there, and we are getting them all. Our focus now -- as our test tells us, is to get just the chapter number. Our missiong, and we have accepted it, is to get just the contents of the chapternumber tag. OK, what's getting the information from the contents entry above? The XSLT looks like this:
[[[<xsl:value-of select="key('titleName', $bookTitle)"/>]]]
What's happening is that the value-of is selecting that whole chapter tag, and then displaying all its text. We want to grab that tag, process what's inside it, and then skip all the items except the chapter number. We'll do that by assigning the selected chapter node to a variable, then processing the templates for that variable, making all of them except the chapternumber one empty, so that they'll do nothing. The result looks like this:
<xsl:template match="chapter/chapternumber"><xsl:apply-templates/></xsl:template>
<xsl:template match="chapter/sectionnumber"></xsl:template>
<xsl:template match="chapter/title"></xsl:template>
<xsl:template match="chapnum">
<xsl:apply-templates select="$chapterLookupDoc"/>
<xsl:variable name="bookTitle" select="@title"/>
<xsl:for-each select="$chapterLookupDoc">
[[[<xsl:variable name="found" select="key('titleName', $bookTitle)"/>
<xsl:for-each select="$found"><xsl:apply-templates/></xsl:for-each>]]]
</xsl:for-each>
</xsl:template>
Note that the templates for chapter/sectionnumber and chapter/title are empty, while the chapternumber one says "apply-templates". That means "process what's inside chapternumber", and what's inside is the text "05.0". So this should nearly work ... and it does, except that there are all kinds of newlines and spaces inside the square brackets. I recognize this as due to the settings that have been sent to XSLT regarding whitespace. Let's see if we can smooth this out. We'll add a statement that tells XSLT to strip out all extraneous whitespace:
<xsl:strip-space elements="*"/>
That statement says that for all elements ("*"), strip out all extraneous whitespace. That may wind up being a bit extreme, but for our purposes right now, it does the job. Our test is green! This is good. However, I don't like the look of that XSLT code inside the square brackets:
[[[<xsl:variable name="found" select="key('titleName', $bookTitle)"/>
<xsl:for-each select="$found"><xsl:apply-templates/></xsl:for-each>]]]
It seems like we should be able to compact that by putting the select-"key" right inside the for-each. I'll try that:
<xsl:template match="chapnum">
<xsl:apply-templates select="$chapterLookupDoc"/>
<xsl:variable name="bookTitle" select="@title"/>
<xsl:for-each select="$chapterLookupDoc">
[[[<xsl:for-each select="key('titleName', $bookTitle)">
<xsl:apply-templates/>
</xsl:for-each>]]]
</xsl:for-each>
</xsl:template>
That's a little better. It seems, though, that we should be able to ditch the inner for-each, and just do the select="key" stuff inside the apply-templates. I'll try that.
<xsl:template match="chapnum">
<xsl:apply-templates select="$chapterLookupDoc"/>
<xsl:variable name="bookTitle" select="@title"/>
<xsl:for-each select="$chapterLookupDoc">
[[[<xsl:apply-templates select="key('titleName', $bookTitle)"/>]]]
</xsl:for-each>
</xsl:template>
That doesn't work: the square brackets are empty. It looks as if no templates were run, certainly not the chapternumber one, or we'd have the right answer. I have no explanation for that, except that somehow the for-each is setting the context and the apply-templates is not. Back that change out, and we're green again! I'll end this chapter with this success, and continue in the next. At the vey end, I'll show the current state of the code and the test files for this last test. But first, let's reflect on what happened.
Before changing that final test to seach for "[[[05.0]]]", I felt that we were thrashing. We were just hacking in random code changes, more or less without direction, trying to get something good to happen. After writing that test, it felt to me that we were more focused, driving step by step to the solution we needed. There was still experimentation and discovery: I don't know XSLT in every detail and frankly it is a very odd language. But it felt to me as if we stayed more "on task" after writing that test.
A good question is whether it was writing the test that did it, or just the fact that right before writing the test, we were reflecting on how things were going. We observed that we were thrashing and we decided to write a better test to help. Maybe we would have proceeded just as effectively by virtue of the reminder to stay on task. There's no way to know for sure.
But I'll take what I can get. I find that whenever I have better tests, my work is more focused and I go off track less often. So it works for me, and you get to decide whether it works for you.
But there were other benefits. Now we have a test that is more focused on what we care about, the lookup of the chapter number, and ignoring the section number and title that are found in the same node in the table of contents. And we have XSLT code that does exactly what we want. And, best of all, it is all documented by a well-focused test. Had we proceeded by just renewing our concentration, we would have a much more vague test. Remember that we used to be just looking at all the output as a big string. Now this test is more focused, and we can probably use the substring search technique in other places as well. We might even extend it to use regular expressions, if we need to. I feel that this new test gives us a bit of a foundation for stronger tests later.
There are two problems that come to mind. First, and easiest, is that I really don't understand quite why the XSLT had to be the way it had to be. We made it work all right, but it was still a bit of slash and burn, not clever refinement based on increasing understanding. That means it's time to read a bit more in the XSLT books, I guess.
Second, we did all the refinement inside one test. Recall that in other chapters, we have done one small test at a time, and all the existing tests continue to work, building up to a more and more robust solution. The old tests help to document our learning path, so that a future reader can read them in order and -- we hope -- follow along in our footsteps. In this example we have lost that history. We wrote the one test, then made it work by doing a few things. But those separate discoveries aren't broken out one at a time by separate tests. To a reader who wasn't with us, the resulting XML files and XSLT file will just show up as an answer, and a fairly large answer, with no sense of how we got there. We'd like the code and tests to be more expressive of what happened, not just the final result.
I don't have a satisfactory pat answer for that concern. To me, the coded tests are a bit better than the manual ones. At least they are repeatable, and they document some of the process if not perfectly. So I think they were worth doing as an investment in keeping on track and in documenting our work. But it's not really good yet. I feel good about what has happened, but I don't have a solid sense of closure. We'll keep this one on the list of things to think about and improve as we go forward with the chapter numbering feature.
And in truth, that's what makes a good day: we're a little smarter and a little better off than we were when we came in. And we have things yet to learn, which means there are more good days coming. See you next time.
using System;
using System.IO;
using System.Xml;
using System.Xml.Xsl;
using System.Xml.XPath;
using NUnit.Framework;
namespace XSLTexperiments {
[TestFixture] public class TestXSLT: Assertion {
private XslTransform transform;
public TestXSLT() {
}
[SetUp] public void SetUp() {
transform = new XslTransform();
}
[Test] public void SimpleTransform() {
StringReader rdr = new StringReader(SimpleInput());
XPathDocument doc = new XPathDocument(rdr);
XPathNavigator nav = doc.CreateNavigator();
StringReader xslStringReader = new StringReader(SimpleXSL());
XmlTextReader xslReader = new XmlTextReader(xslStringReader);
transform.Load(xslReader);
String result = Transform(nav);
AssertEquals(ExpectedString(), result);
}
[Test] public void SimpleFileTransform() {
XPathDocument doc = new XPathDocument("..\\..\\simpleinput.xml");
XPathNavigator nav = doc.CreateNavigator();
transform.Load("..\\..\\simplexsl.xsl");
String result = Transform(nav);
AssertEquals(ExpectedString(), result);
}
[Test] public void SimpleLookupTransform() {
XPathDocument doc = new XPathDocument("..\\..\\chapter.xml");
XPathNavigator nav = doc.CreateNavigator();
transform.Load("..\\..\\Chapter.xsl");
String result = Transform(nav);
int begin = result.IndexOf("[[[");
int end = result.IndexOf("]]]");
String found = result.Substring(begin,end - begin + 3);
AssertEquals("[[[05.0]]]", found);
}
private String Transform(XPathNavigator nav) {
StringWriter w = new StringWriter();
transform.Transform(nav, null, w);
return w.ToString();
}
private String SimpleInput() {
String s =
@"<?xml version=""1.0""?>
<title>
</title>";
return s;
}
private String SimpleXSL() {
String x =
@"<xsl:stylesheet version=""1.0""
xmlns:xsl=""http://www.w3.org/1999/XSL/Transform"">
<xsl:template match=""title"">
Title Seen
</xsl:template>
</xsl:stylesheet>";
return x;
}
private String ExpectedString() {
String r =
@"<?xml version=""1.0"" encoding=""utf-16""?>
Title Seen
";
return r;
}
public static void Main() {
}
}
}
<?xml version="1.0" encoding="utf-8" ?>
<page>
<header>
<chapnum title="Title"/>
<title>Title</title>
<author>Ron</author>
</header>
<P>This is some paragraph.</P>
</page>
<?xml version="1.0"?>
<page>
<chapter name="Something">
<sectionnumber>0</sectionnumber>
<chapternumber>00.0</chapternumber>
<title>Something</title>
</chapter>
<chapter name="Title">
<sectionnumber>0</sectionnumber>
<chapternumber>05.0</chapternumber>
<title>Title</title>
</chapter>
</page>
<?xml version="1.0" encoding="utf-8" ?>
<xsl:stylesheet version="1.0"
xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
<xsl:strip-space elements="*"/>
<xsl:variable name="chapterLookupDoc" select="document('TOC.xml')"/>
<xsl:key name="titleName" match="chapter" use="@name"/>
<xsl:template match="chapter">
</xsl:template>
<xsl:template match="chapter/chapternumber"><xsl:apply-templates/></xsl:template>
<xsl:template match="chapter/sectionnumber"></xsl:template>
<xsl:template match="chapter/title"></xsl:template>
<xsl:template match="chapnum">
<xsl:apply-templates select="$chapterLookupDoc"/>
<xsl:variable name="bookTitle" select="@title"/>
<xsl:for-each select="$chapterLookupDoc">
[[[<xsl:variable name="found" select="key('titleName', $bookTitle)"/><xsl:for-each select="$found"><xsl:apply-templates/></xsl:for-each>]]]
</xsl:for-each>
</xsl:template>
</xsl:stylesheet>