I've got a little time here, and there are those tests set to Ignore. Let's fix them so we don't have to ignore them.

Trusting the Tests

Working yesterday, I felt a lot of trust in the tests. That’s probably because I just worked on one class and it’s rather nicely tested. But as I’ve reported, when working on the whole blog program, I don’t feel that same trust, and often run the program on my local computer to see if it works. This is not a good thing.

Ricia’s working late tonight, and it’s too early to go foraging for food, so I’ll take a look at which tests are ignored and see what it takes to make them run. I think that mostly they are failing because they run against the Pages directory on my computer, and when I change or add any files in testing, it messes up details of the tests. We’ll see …

One of the files with an Ignore in it is TestSubstitution. It’s not interesting, it’s just a test that I wrote for a new feature but don’t want to work on it yet. And the NUnitAsp test is turned off because it runs forever. I’ll look at that last, to see whether there’s a way to get the test done without launching a browser. That leaves TestRSS. Two of its methods, FormatList and RssItems, are ignored. Here’s the info:

    // following test changes if the files mentioned are changed!
    // TODO consider mocking the new FileInfo wrapper if we do one

    [Test ] [Ignore ("need a test that allows editing")] public void FormatList() {
      ArticleList list = new ArticleList(SampleArticles());
      string contents = list.Contents();
      string expected = "8/13/2004 ''MyBusinessIsToCreate''\r\n8/9/2004 ''BlogStories''\r\n";
      AssertEquals(expected, contents);
    }

    [Test] [Ignore ("need a test that allows editing")] public void RssItems() {
      ArticleList list = new ArticleList(SampleArticles());
      string[] rssItems = TestArticle.SplitLines(list.RssItems());
      int line = 0;
      AssertEquals("<item>", rssItems[line++]);
      AssertEquals("<title>My Business Is To Create</title>", rssItems[line++]);
      AssertEquals(
        "<link>http://www.XProgramming.com/Blog/Page.aspx?display=MyBusinessIsToCreate</link>",
        rssItems[line++]);
      AssertEquals(
        "<description><![CDATA[<B>My Business Is To Create</B>",
        rssItems[line++]);
      line += 7;
      AssertEquals("]]></description>", rssItems[line++]);
      Assert(rssItems[line++].StartsWith("<pubDate>"));
      AssertEquals("</item>", rssItems[line++]);
      AssertEquals("<item>", rssItems[line++]);
      AssertEquals("<title>Blog Stories</title>", rssItems[line++]);
      AssertEquals(
        "<link>http://www.XProgramming.com/Blog/Page.aspx?display=BlogStories</link>",
        rssItems[line++]);
      AssertEquals(
        "<description><![CDATA[<B>Blog Stories</B>",
        rssItems[line++]);
      line += 13;
      AssertEquals("]]></description>", rssItems[line++]);
      Assert(rssItems[line++].StartsWith("<pubDate>"));
      AssertEquals("</item>", rssItems[line++]);
    }

These fail, as the comment suggests, because the page directory has been changed by manual work with the product. I’ll turn off the Ignore and make sure that I know what’s up. Sure enough, they’re failing because the files have changed underneath them. First, I’ll fix the tests to match the files.

Whoa! I was going to do that by editing the files so that their dates would be in the order of My Business Is To Create first, then Blog Stories, so that the editing of the test would be less extensive. However, the program isn’t working at all! It comes up with nothing but one line of garbage in the table of contents! My choices are to divert to fix that problem, or to make these tests run first. I think I’ll stay on task, because I think the tests will be easy enough to make work and having them turned on might help me. But now I owe myself a test because of this bug. Hold me to that promise!

I’ll touch the “My Business” file to get its date earlier and then fix the tests. A new one breaks, Date, and getting the files in the right order fixes the RssItems one. That’s good, I’d hate to have to mess with it. Though I am wondering why it works: I think because I explicitly removed date considerations from that test already. Anyway, here are the fixed tests:

    [Test] public void Date() {
      FileInfo f = new FileInfo("../Pages/MyBusinessIsToCreate");
      Assert(f.Exists);
      AssertEquals("9/7/2004", f.LastWriteTime.ToShortDateString());
    }

    [Test ] public void FormatList() {
      ArticleList list = new ArticleList(SampleArticles());
      string contents = list.Contents();
      string expected = "9/7/2004 ''MyBusinessIsToCreate''\r\n9/2/2004 ''BlogStories''\r\n";
      AssertEquals(expected, contents);
    }

All I did in these was to change the dates to 9/7 and 9/2 from whatever they were. Now these tests will run until the next time I change a file in the Pages directory. Let’s see if we can fix that. My idea is to make a directory that just has the files in it as they stand now, and to leave it alone in the future. That will keep the tests stable. It does mean, however, that if I change how the RSS stuff works, I’ll have to change the files in that folder. For now, I’m not worried about that, so let’s go ahead.

The culprits are two methods in the test that refer to the Pages directory:

    [Test] public void Date() {
      FileInfo f = new FileInfo("../Pages/MyBusinessIsToCreate");
      Assert(f.Exists);
      AssertEquals("9/7/2004", f.LastWriteTime.ToShortDateString());
    }

    private ArrayList SampleArticles() {
      ArrayList result = new ArrayList();
      result.Add(new FileInfo("../Pages/MyBusinessIsToCreate"));
      result.Add(new FileInfo("../Pages/BlogStories"));
      return result;
    }

I’ll just change them to refer to the TestPages folder instead and that should do the job as soon as I copy the files to it. As planned, that breaks the tests when the folder isn’t there, but if I create it and put the files in it … yes, now the tests run. Now all my tests are green except for the one that launches the browser (which might be worth a look just now) and the one for the feature not yet on my plate.

Find a Defect, Write a Test

That’s what I teach, so it had better be what I do, at least while you folks are watching. We have a major defect, namely that the table of contents is displaying nothing good. I’m sure it will be easy to find, but we’d better write a test for it along the way.

A quick manual test shows me that the table of contents is coming up wrong, that regular articles and even invisible files come up OK, but that the wiki links in those files show up with question marks, not as hyperlinks, even when the file exists. Probably this is something about all that neat work we did last night with the file naming.

Some quick work in the debugger tells me that this code is confused and returning the wrong file list:

    private string TableOfContentsText() {
      return ContentsList().Contents();
    }

    private ArticleList ContentsList() {
      DirectoryInfo info = new DirectoryInfo(articleFiler.DirectoryName);
      FileInfo[] files = info.GetFiles("*.");
      return new ArticleList( new ArrayList(files));
    }

In particular, we find that articleFiler.DirectoryName is “c:". This strikes a familiar note with me, and sure enough we find that DirectoryName looks like this:

    public string DirectoryName {
      get { return Path.GetPathRoot(basicFileName); }
    }

Yes, well. GetPathRoot seems to have returned the root of the C directory. Not too surprising when you think about it. That should definitely say GetDirectoryName. In fact, I tried it. So sue me.

Maybe you should. But I am going to write the test, because you’re watching. If you weren’t watching, I’d be mighty tempted to just ship the fix because I “know” it isn’t going to break again. That would be bad. But first, and I trust you’ll forgive me for this, I’m going to dinner.

Back on the Case

OK. Blackened Black and Blue Salad, thanks for asking. Caesar dressing, salad, 8 oz. steak, blackened, medium rare, blue cheese crumbles. Zukey Lake Tavern. Now let’s get to it. First, I’ll change the code back to the Root thing, so it’s broken, then I’ll write a test to prove it.

    [Test] public void DirectoryName() {
      AssertEquals (
        @"C:\inetpub\wwwroot\Blog\Pages",
        Filer("../Pages", "WikiTest").DirectoryName);
    }

I’m not entirely sure about that path, but I think it’s right. And the test fails now, with the returned value being “C:" just as we expected. Now to apply the fix …

Wow! It answered “..\Pages”. So GetDirectoryName isn’t the right call either. See why we write these tests? (Now, as far as I can tell, this path would work just fine, but I was really wanting DirectoryName to return the full path, so let’s see if we can make it work.) Well, I just tried GetFullPath. That tells me that the inetpub directory is capitalized, but it also returns the rest of the file name, namely “WikiTest”. I don’t want that part of the name. Hmm …

Grrr … there’s no method that does it. We’ll have to use the Path class again. This does it:

    public string DirectoryName {
      get {
        string fullPath = Path.GetFullPath(basicFileName);
        return Path.GetDirectoryName(fullPath);
      }
    }

We get the full file name, then pull the directory name out of it. An alternate way to handle it might be to use the full file name throughout, or to accept the fact that the method can return a shorthand directory if we pass one in. I think I’ll go with that: it should be harmless. Here’s the final code, and it runs all the tests:

    [Test] public void DirectoryName() {
      AssertEquals (
        @"..\Pages",
        Filer("../Pages", "WikiTest").DirectoryName);
    }

    public string DirectoryName {
      get { return Path.GetDirectoryName(basicFileName); }
    }

OK. One more thing. When we find a defect, we write a test. We are also supposed to learn from the test. What did we learn? What else could go wrong? Right! The ArticleName method. We’ll modify and extend the test:

    [Test] public void ArticleName() {
      AssertEquals (
        "WikiTest",
        Filer("../Pages", "WikiTest.wiki").ArticleName);
    }

And that one works. Note that I gave the input file an extension to make sure that we got back the article name, not the file name.

Draining the Swamp

Well, we got up to our tail in alligators there, and almost forgot that we came here to drain the swamp. We’ve got all the tests working, but the ones we have just been looking at are also pointed at the Pages directory. I think we’d like to change them to point to the TestPages directory, so that they won’t go unstable on us. I’ll make the substitution and see if they still run. Since I copied over all the files, they should.

OK, change made, and they all work. Our work here is done and more than done.

Reflection

Our purpose was to take the Ignored tests out, and we accomplished that. We made the strategic decision to keep the tests stable by looking at a directory specifically set up for testing purposes. (We could have done something clever with a MockObject, I suppose, but this seems more straightforward given the situation. If we were sharing this program over several computers, I might go another way.)

Along the way, we discovered that our tests had failed us, and the blog couldn’t display its table of contents. We also noticed that wiki links weren’t working. We haven’t written a test for that yet, nor are we sure whether it’s fixed. I’ll check that manually … and I’m happy to report that it does. However, this tells me that the WikiNameTagger, which I believe we haven’t looked at yet, isn’t well tested, since that directory problem should have made any decent test of that object fail. I guess I’m not done yet, though I could ship now if need be.

What we find in WikiNameTagger tests is that they check all the substitutions that insert anchor tags wherever they go, but that they don’t check WikiNameTagger’s existence logic, which looks like this:

    private string LinkSubstitution(Match match) {
      string name = match.Groups[1].Value;
      if ( name.StartsWith("!") )
        return name;
      ArticleFiler testArticleFiler = new ArticleFiler(path, name);
      if ( testArticleFiler.Exists )
        return ExistingLinkSubstitution(match);
      else
        return MissingLinkSubstitution(match);
    }

What we’re doing here is that we’re passed a path, and we create a new ArticleFiler on that plus the name we’ve identified in the text, and check to see if it Exists. That code won’t work if the path is bad (and it was). I’m inclined to test it directly if I can figure out a good way.

Yes, here’s a test:

using NUnit.Framework;

namespace Blog {
  [TestFixture] public class TestWikiNameTagger : Assertion {
    [Test] public void Hookup() {
      Assert(true);
    }

    [Test] public void TwoTags() {
      string text = "Tag BlogStories yes WikiNothing no";
      WikiNameTagger tagger = new WikiNameTagger(@"..\TestPages");
      string result = tagger.TagWikiNames(text);
      string expected = "Tag <a href=\"Page.aspx?display=BlogStories\">Blog Stories</a> yes WikiNothing<a href=\"Edit.aspx?display=WikiNothing\">?</a> no";
      AssertEquals(expected, result);
    }
  }
}

This runs, and perhaps more interesting, it found a problem. The code as written wasn’t putting the “display=” on the “Edit.aspx?” string. My manual tests didn’t find that, because the Edit feature on undefined wiki words doesn’t work yet, so I wasn’t surprised when it didn’t work. So, now is our work done? Let’s reflect again.

Reflection, Mark II

The tests feel much stronger to me now. I’ve found a few defects and written tests for them and fixed the defects. I’ve looked around, found things needing improving, and fixed them. Not bad for a couple of hours work.

But do I now trust the tests completely? Not yet. Chet and I will probably be pairing tomorrow. We’ll try to figure out why we don’t trust them, and change them so that we do. We might have to turn the slow one back on, but if we do, then we should figure out a way to make it run fast.

And then, I want to get back to the idea of cleaning up the way the ArticleFiler is used. J. B. Rainsberger suggested that it should be passed in to the Article as a parameter instead of being held on to. We might try that. For now, though, it’s a good Session. Thanks for dropping by.