Using TDD when you don't know the technology

published: Mon, 2-May-2005   |   updated: Thu, 27-Oct-2005

This came up in a discussion at work. "We need to write code to access technology X, which uses COM. How do we do it using TDD?" Technology X is very deep and rich and looking at the API is an exercise in frustration: how to we get to where we want to be? The task seems overwhelming.

My reply went along these lines. First write a test that instantiates one of the COM objects from technology X. That's all. Compile and run it. If it doesn't pass, investigate why and try again. If it passes, move on to the next bit of functionality. Maybe the instance has some properties. What are their values in this just-instantiated state? Write small tests for each, and move on to test the methods. And so on, so forth.

Here's an example. The Office Assistants (you know, Clippy et al that are used in Microsoft Office) are distilled in a COM object that you can use (it's called Microsoft Agent Control). The amount of information that's available to show you how you can use these assistants is pretty light and mostly involves VB6. Urk, but at least it shows you what's possible. My goal is to display one in a C# app.

I started a new application in VS2005. For fun I made it a console application. I added NUnit.Framework as a .NET reference, and then I added Microsoft Agent Control as a COM reference.

First test: instantiate one of these critters.

  [TestFixture]
  class TestMsAgentAccess {
    [Test]
    public void InstantiateAgent() {
      Agent a = new AgentClass();
      Assert.IsNotNull(a);
    }
  }

That was a total guess by the way. I looked for a factory class or method to get an instance of Agent (despite its name, an interface). Nothing. The only class around was called AgentClass, so I tried that.

The test compiles, runs and passes. Whoopie, etc, I instantiated something.

Let's try and do something with an Agent instance. The Characters property looks interesting.

    [Test]
    public void GetCharacters() {
      Agent a = new AgentClass();
      IAgentCtlCharacters chars = a.Characters;
      Assert.IsNotNull(chars);
    }

Compile this one and run. Kablooie: a COM exception. HResult is some bizarre hex number that I don't understand. Mmmm. I look through the Intellisense of members for my instance. There's one called Connected; it sounds interesting, so let's try it.

    [Test]
    public void GetCharacters() {
      Agent a = new AgentClass();
      a.Connected = true;
      try {
        IAgentCtlCharacters chars = a.Characters;
        Assert.IsNotNull(chars);
      }
      finally {
        a.Connected = false;
      }
    }

Compile, run, and it passes. Heh.

Time to check out what's in the array of characters: are any loaded be default?

    [Test]
    public void IterateCharacters() {
      Agent a = new AgentClass();
      a.Connected = true;
      try {
        IAgentCtlCharacters chars = a.Characters;
        Assert.IsNotNull(chars);
        int count = 0;
        foreach (IAgentCtlCharacterEx ch in chars) {
          count++;
        }
        Assert.AreEqual(0, count);
      }
      finally {
        a.Connected = false;
      }
    }

I guessed that there aren't any, and that's what's borne out by running the test. (By the way, that's a pattern for writing unit tests for some class you haven't written. Guess the answer. If it's wrong, the test will tell you the right answer. If you understand why the answer has the value it does -- maybe another test should validate your assumption -- then change the test to use the correct value.)

Let's load a character (that is, an assistant -- I use Links the cat, myself) then.

    [Test]
    public void LoadCharacter() {
      Agent a = new AgentClass();
      a.Connected = true;
      try {
        IAgentCtlCharacters chars = a.Characters;
        Assert.IsNotNull(chars);
        chars.Load("cat", @"c:\blahblah\OFFCAT.ACS");
        IAgentCtlCharacterEx cat = chars["cat"];
        Assert.IsNotNull(cat);
      }
      finally {
        a.Connected = false;
      }
    }

Compile, run, and it passes. We're really racing ahead now.

The next step is a little difficult to encapsulate as a unit test: we're going to show the assistant and get it to speak. The bast way I know of to do that is to see that it shows up.

  class Program {
    static void Main(string[] args) {
      Agent a = new AgentClass();
      a.Connected = true;
      try {
        a.Characters.Load("cat", @"c:\blahblah\OFFCAT.ACS");
        IAgentCtlCharacter cat = a.Characters["cat"];
        Console.WriteLine("showing assistant");
        cat.Show(false);
        try {
          cat.Speak("hello, world", "");
          Console.ReadLine();
        }
        finally {
          cat.Hide(false);
        }
      }
      finally {
        a.Connected = false;
      }
    }
  }

And lo, Links, the cat assistant, shows up and says "hello, world".

Now, it must be admitted that this is not the be all and end all of using an Office Assistant in your own .NET application. It's just a start. There's possibly several things wrong with my code as it stands at the moment, but that's not the point of this exercise. It took me 15 minutes google time, and 45 minutes writing this small set of tests and this article at the same time. Yet, I feel as if I've cracked the basics of using an Office Assistant in my C# code.

Now what should happen is that I should refactor my test code (there's an awful lot of duplication that's stinking up the code). Then probably isolate the access to the Microsoft Agent Control with a wrapper and an interface, so that I can test things like "am I calling Connected = false for every Connected = true?", "am I calling Hide() for every Show()?" and so on by using mock objects.

The moral of the tale, though, is this: if you are unsure of how to write a unit test for something, you are trying to test too much. Ratchet down your TDD red-green-refactor cycle to the smallest it can be until you gain the confidence in the technology you are using. You'll find that you learn about the technology much more quickly that trying to write something over-arching. And as you go through the technology, you'll find that you have the fine-grained tests to back up your assumptions.