XProgramming > XP Magazine > Adventures in C#: The First Customer Test
COLLECTED TOPICS: Kate Oneal | Adventures in C# | Documentation in XP | Book Reviews
Adventures in C#: The First Customer Test
Ron Jeffries
08/19/2002
XP teams very often don't have customer (acceptance) tests, or leave them until "later". The result is a less confident customer, and problems when you least need them. Here's our first customer test. See? That wasn't hard.

Contents:

Recap

With our conflicting schedules, Chet and I haven't worked on our XML Notepad for over a week. After a delicious lunch at California Pizza Kitchen, we went to the Michigan Union to write a little code. A quick review of the tests was all it took to get us back on track. The last test in the code base was this one, a precursor to a real customer test, in CustomerTest.cs:

    [Test] public void ArrayInput() {
      String commands = 
@"*input
some line
*end
*enter
*display
*output
some line

<P>|</P>";
      InterpretCommands(commands);
    }

What we remembered was that we were on the path of setting things up so the customer could type in files contining little scripts like the one in "commands", and there would be a generic test method that would run them all. We hadn't, as yet, figured out how to do that: we started by just writing a script as a string and making it run.

Reviewing this test, we noticed that it isn't array input, it's string input, so we renamed it:

    [Test] public void StringInput()

Now here's what we've done so far. This test takes a series of little commands, "*input", "*enter", and "*output". The input command primes a TextModel with the provided input, the enter command causes the model to do its keyboard Enter action, and the output command reads the text from the TextModel and compares it to the provided output. We have written a tiny command interpreter, using just a bunch of if statements, to handle this language. I plan to come back to this topic, but just a mention here:

We could have been saying here that we can't even unit test this object because the program is just a GUI program. Instead, we extracted the functional logic to a TextModel, for which we could write tests. We're not very good at C# yet, and we'll probably wind up making the connection between the Form and the TextModel better as we learn, but we got it extracted, and tested, with no big problems. Review the earlier articles for that story.

Similarly, we are programming this for ourselves -- we are the customer -- and we could have skipped the Customer Tests, using unit tests instead. But we have a purpose here, which is to show you how we'd write a real application, and that would include Customer Tests. So we wrote this very simple command interpreter that supports the test StringInput, just above. To save you looking back, here's that code:

    private void InterpretCommands(String commands) {
      StringReader reader = new StringReader(commands);
      String line = reader.ReadLine();
      while ( line != null) {
        if ( line == "*enter")
          model.Enter();
        if (line == "*display")
          Console.WriteLine("display\r\n{0}\r\nend", model.TestText);
        if (line == "*output") 
          CompareOutput(reader);
        if (line == "*input")
          SetInput(reader);
        line = reader.ReadLine();
      }
    }

Not a very sophisticated or beautiful interpreter, but it works just fine. We'll see whether it needs to be improved -- later.

Remarks on Technique

Before we move along, we'd like to underline what we did. Our real purpose is to have a directory full of Customer Test scripts, and to run them all automatically with NUnit, and to have them report results in some useful way. We could have started by finding out how to read file names from a directory, and how to read lines from a file -- these are things that we don't know how to do in C#, and that are necessary to the process. But we didn't do that. Why, you might ask ...

We didn't do those things because, although we don't know exactly how to do them, there's clearly no big mystery. I can tell you right now what the code will look like:

Get a list of files matching test names from some directory.
Loop over the list.
Read each file.
Treat it as a test
   accept input
   accept editor commands like Enter
   check output, i.e. the current contents of the TextModel

There's no mystery to the directory and file part of that, even though we don't know any of the details. The mystery is in how we will process the file contents to do commands. Therefore, that's the part we solved first. The result was the ArrayTest. This is, we suggest, a good general rule.

The Learning Rule

When creating a new capability, work first on the part where you'll learn the most about the real shape of the problem.

There's only one problem with this rule. There's another rule that goes this way:

The Difficulty Deferral Rule

Solve what you know, leave the parts you don't know until later.

Maybe later on we'll use that rule. We'll try to see if we can give some guidelines about when to do that. Right now, the learning rule seems more important.

Moving right along ...

Test from a File

Since we're trying to write a Customer test here, we envision that the customer will give us little scripts, like the one in StringInput, in files. We created a file by pasting the StringInput data into a text file, and we wrote this new test:

    [Test] public void FileInput() {
       StreamReader stream = File.OpenText("c:\\data\\csharp\\notepad\\fileInput.txt");
       String contents = stream.ReadToEnd();
       stream.Close();
       InterpretCommands(contents);
     }

We had to look up some file stuff, since we haven't done much file reading, but the test above worked the first time. Sophisticated C#ers will notice that we didn't use the @"string" technique to avoid doubling those backslashes. That's because we didn't know it. I just read about it last night, and I'll fix it today.

Test a Lot of Files

Well, OK, now our Customer can type test files and the system can run them, except that we don't want to have to write a new test like FileInput every time the Customer adds a test. So we have one more test in mind: one that finds all the files and runs them. We decided that using ".txt" as the test file suffix was a bad idea, so we resaved the file as ".test", and changed the FileInput test accordingly. Then we fiddled around until we found the Directory class, and wrote this test:

    [Test] public void TestAllFiles() {
      String[] testFiles = Directory.GetFiles("c:\\data\\csharp\\notepad\\", "*.test");
      AssertEquals(1, testFiles.Length);
      foreach (String testFile in testFiles) {
        InterpretFileInput(testFile);
      }
    }
    
    private void InterpretFileInput(String fileName) {
      StreamReader stream = File.OpenText(fileName);
      String contents = stream.ReadToEnd();
      stream.Close();
      InterpretCommands(contents);
    }

We worked up to this with an intermediate assert in the test, which is still shown. Since we weren't sure what would come back from the Directory.GetFiles, we asserted that it would be of length one. Sure enough it was, and we'll need to take that test out shortly. This test worked quite well, except that something odd happened.

We had originally named InterpretFileInput as TestFileInput. It made sense at the time. However, even though NUnit does that thing with the [Test] attributes, it also finds all methods in subclasses of Assertion that start with "Test". So the method was being run, and it would loop instead of throwing an exception or failing. We didn't see that it wasn't our new TestAllFiles method (can't read, I guess) and we spent a bunch of time trying to figure out why it was looping. Once we saw the problem, we just renamed the method and everything was just fine.

What We Learned

We have a Customer Test! Now a non-programmer (us, the way we're doing here) can enter files named something.test and put little scripts in there, to test things. It wasn't very hard, only a couple of hours work all together, and now with our Customer hat on, we can test the system. There were only a few necessary steps:

Extract the Model

We elected to break out the functional part of the program into a separate object that is "just code" and no GUI. Some people talk about instantiating forms and talking to them, and maybe we'll experiment with that as well. But it seems right to break out the model anyway. You've seen that article and we hope it seemed as natural and easy to you as it did to us.

Work Up to It

We started with that DirectInput test, which was nearly trivial. It just sent Enter to an empty TextModel and checked the output directly. That was enough to drive us to build the basic connections. The trickiest bit was the little command language. And notice how we skipped all the things that could make a command language difficult. We have no syntax to speak of, just lines that start with * and have a command word. We have no parser, just code that looks at the line to see if it is a command. A loop and some if statements.

Then we went a step further, and wrote the ArrayInput test (renamed to TestInput) that worked from a string. Naturally, we were thinking we'd get the string from a file some time soon. Then read one file, then read them all. Inch my inch, step by step, slowly we turned an empty program into a Customer Test.

And you can, too.

Still To Come

This is just one test, but we can hand the tool to our Customer now (that's us with our customer hat on, but a real human could do it) and the Customer can write more tests. We know that when he does, we'll get some requests. We'll have a need to set the cursor position. We might have a need to have incorrect files report an error, such as files that do no checking of *output. And of course there will be new editing instructions, and new commands.

We already know what the next story will be. Our customer wants us to implement a File menu, with Save and New and Open. We'll probably split that story and do Save first. Probably before that happens, though, Customer will write some more tests and break our tiny framework. But that's OK, because we don't have a framework: We have tests. and that's the bottom line:

You don't need a framework, you need tests!

XProgramming > XP Magazine > Adventures in C#: The First Customer Test
COLLECTED TOPICS: Kate Oneal | Adventures in C# | Documentation in XP | Book Reviews