My Cunning Plan


After a little reflection on how to test this, I came to the conclusion that using the Customer Acceptance Tests is the best approach. The customer tests are the only ones that create a real, operable XML Notepad, so they are the best place to put the new tests for the file operations. As well, I know that I must have customer tests for this capability, so I ll save some duplicated effort. My plan is to do those tests by providing new customer commands to save and load the XML Notepad.

The choice now is whether to do the tests first and let them drive the necessary changes or to complete the changes and then test them. It s always better to do the tests first, and since I ve been derelict in not doing them soon enough, let s do the tests now. We ll review the CustomerTest code first:

 using System; 
using System.IO;
using System.Windows.Forms;
using System.Collections;
using NUnit.Framework;
namespace Notepad
{
[TestFixture] public class CustomerTest : Assertion {
private TextModel model;
private XMLNotepad form;
[SetUp] public void CreateModel() {
model = new TextModel();
form = new XMLNotepad(model); }
[Test] public void EmptyModel() {
form.XMLKeyDownHandler((object) this, new KeyEventArgs(Keys.Enter));
AssertEquals("<P></P>\r\n", model.TestText);
}
[Test] public void DirectMenu() {
form.MenuForAccelerator("&P").PerformClick();
AssertEquals("<pre></pre>\r\n", model.TestText);
}

[Test] public void StringInput() {
String commands =
@"*input
some line
*end
*enter
*output
some line
<P></P>";
InterpretCommands(commands, "");
}
[Test] public void FileInput() {
InterpretFileInput(@"c:\data\csharp\notepad\fileInput.test");
}
[Test] public void TestAllFiles() {
String[] testFiles = Directory.GetFiles(@"c:\data\csharp\notepad\",
"*.test");
foreach (String testFilename in testFiles) {
InterpretFileInput(testFilename);
}
}
private void InterpretFileInput(String fileName) {
StreamReader stream = File.OpenText(fileName);
String contents = stream.ReadToEnd();
stream.Close();
InterpretCommands(contents, fileName);
}
private void InterpretCommands(String commands, String message) {
StringReader reader = new StringReader(commands);
String line = reader.ReadLine();
CreateModel();
while ( line != null) {
if ( line == "*enter")
form.XMLKeyDownHandler((object) this, new KeyEventArgs(Keys.Enter));
if ( line == "*shiftEnter")
form.XMLKeyDownHandler((object) this,
new KeyEventArgs(Keys.Enter Keys.Shift));
if ( line == "*altS") ExecuteMenu(" & S");
if ( line == "*altP")
ExecuteMenu(" & P");
if (line == "*display")
Console.WriteLine("display\r\n{0}\r\nend", model.TestText);
if (line == "*output")
CompareOutput(reader, message);
if (line == "*input")
SetInput(reader);
line = reader.ReadLine();
}
}
private void CompareOutput(StringReader reader, String message) {
String expected = ExpectedOutput(reader);
String result = model.TestText;
if (expected != result) {
Console.WriteLine(message);
Console.WriteLine("*Expected");
Console.WriteLine(expected);
Console.WriteLine("*Result");
Console.WriteLine(result);
}
AssertEquals(message, expected, model.TestText);
}
private String ExpectedOutput(StringReader reader) {
return ReadToEnd(reader);
}
private String ReadToEnd(StringReader reader) {
String result = "";
String line = reader.ReadLine();
while (line != null && line != "*end") {
result += line;
result += System.Environment.NewLine;
line = reader.ReadLine();
}
return result;
}
private void SetInput(StringReader reader) {
InputCommand input = new InputCommand(reader);
model.Lines = input.CleanLines();
model.SelectionStart = input.SelectionStart();
form.CustomerTestPutText();
}
private void ExecuteMenu(string accelerator) {
form.MenuForAccelerator(accelerator).PerformClick();
}
}
}

Our mission, and we have decided to accept it, is to add commands to the interpreter, implement them, and make them work. There s one special issue to deal with: I really don t want to pop up a file dialog in the middle of the tests, so we ll have to provide the file name somehow as part of the command. We ll see how that goes. The lines in boldface in the code just shown are the core of command interpretation. Let s add a loadfile and savefile command:

 private void InterpretCommands(String commands, String message) { 
StringReader reader = new StringReader(commands);
String line = reader.ReadLine();
CreateModel();
while ( line != null) {
if ( line == "*enter")
form.XMLKeyDownHandler((object) this, new KeyEventArgs(Keys.Enter));
if ( line == "*shiftEnter")
form.XMLKeyDownHandler((object) this,
new KeyEventArgs(Keys.Enter Keys.Shift));
if ( line == "*altS")
ExecuteMenu("&S");
if ( line == "*altP")
ExecuteMenu("&P");
if (line == "*display")
Console.WriteLine("display\r\n{0}\r\nend", model.TestText);
if (line == "*output")
CompareOutput(reader, message);
if (line == "*input")
SetInput(reader);
if (line == "*loadfile")
LoadFile();
if (line == "*savefile")
SaveFile();
line = reader.ReadLine();
}
}
private void SaveFile() { form.SetFileName("customertestxmlfile.xml");
ExecuteMenu("^S");
}
private void LoadFile() { form.SetFileName("customertestxmlfile.xml");
ExecuteMenu("^O");
}

The idea so far is that the *loadfile command and the *savefile command will privately set the file name in the form to the file name shown above and then execute the menu associated with Ctrl+O or Ctrl+S, respectively. I m going to make the hat character ^ mean control, just as & now means alt. Making this test run will be a bit tricky, I think, but it should all come down to getting access to the right menu item and executing its code. I happen to remember that the MenuForAccelerator code that supports ExecuteMenu is quite weak right now ”that will have to be improved a lot. This code doesn t compile because SetFileName() isn t defined. I ll implement it this way, in XMLNotepad:

 internal void SetFileName(String name) { 
fileName = name;
}

The tests all run, so our next step is to write a customer test using these new commands and make it fail. Then we ll make it work. The first part should be easy:

 *input 
<P>uno</P>
<P>duo</P>
<P>tres</P>
*end
*savefile
*input
*end
*output
*end
*loadfile
*output
*enter
*output
<P>uno</P>
<P>duo</P>
<P>tres</P>
*end

In this test, we enter three lines and save them. Then we give the model new empty input and check that it s empty. Then we load the file back up and check that we get the same thing back. I ll save that file and run the tests, expecting big trouble of some kind.

Well, I got big trouble, but I don t know what it means. I have an exception in the test, and the message says TestAllFiles: ˜-2 is not a valid value for ˜value . Oh, OK, that s referring to the code that sets the selection. I didn t put the vertical bar anywhere in the input, so it couldn t set the cursor. I ll put it at the end of the last line:

 *input 
<P>uno</P>
<P>duo</P>
<P>tres</P>
*end
*savefile
*input
*end
*output
*end
*loadfile
*output
*enter
*output
<P>uno</P>
<P>duo</P>
<P>tres</P>
*end

That didn t fix it. I m going to have to resort to the debugger. I hate that, but I see no easier choice. I m a little concerned that the defect isn t coming from where I think it is, even though it seems like it has to be. No, wait. I see it. The program can t deal with empty input. I ll have to put a line into the test in the middle:

 *input 
<P>uno</P>
<P>duo</P>
<P>tres</P>
*end
*savefile
*input
<P><P>
*end
*output
<P><P>
*end
*loadfile
*output
*enter
*output
<P>uno</P>
<P>duo</P>
<P>tres</P>
*end

That gives me an error that s more like what I expect:

 c:\data\csharp\notepad\saveload.test 
*Expected
*enter
*output
<P>uno</P>
<P>duo</P>
<P>tres</P>
*Result
<P><P>
<pre></pre>

I m a little surprised there were no exceptions. What happened , however, was due to the weak code in MenuForAccelerator. We ll fix that and see what happens next. Here s the old code:

 public MenuItem MenuForAccelerator(string accelerator) { 
if (accelerator == "&S") return insertSection;
return insertPre;
}

This isn t quite up to our needs. It understands &S only and then assumes that if it isn t S, it s P. We need to make that a lot stronger. Fortunately, we can do that pretty readily:

 public MenuItem MenuForAccelerator(string accelerator) { 
if (accelerator == "&S") return insertSection;
if (accelerator == " & P") return insertPre;
if (accelerator == "^O") return openFile;
if (accelerator == "^S") return saveFile;
return null;
}

I ve changed it to return null if it doesn t find the menu sought. That should make any further mistaken tests fail in a more obvious way. This code won t compile, however, until I make openFile and saveFile member variables. Recall that they were temporary variables in the form initialization up until now. I ll promote them and change the init:

 class XMLNotepad : Form { 
public TestableTextBox textbox;
private TextModel model;
private MenuItem insertPre;
private MenuItem insertSection;
private MenuItem openFile;
private MenuItem saveFile;
private ModelAction enterAction;
private ModelAction shiftEnterAction;
private ModelAction insertSectionAction;
private ModelAction insertPreTagAction;
private ModelAction saveAction;
private String fileName; public delegate void ModelAction();
public MenuItem MenuForAccelerator(string accelerator) {
if (accelerator == "&S") return insertSection;
if (accelerator == "&P") return insertPre;
if (accelerator == "^O") return openFile;
if (accelerator == "^S") return saveFile;
return null;
}
private void initialize(TextModel model) {
InitializeDelegates(model);
this.model = model;
this.Text = "XML Notepad";
MenuItem fileMenu = new MenuItem("&File");
MenuItem newFile = new MenuItem("&New");
newFile.Click += new EventHandler(MenuFileNewOnClick);
newFile.Shortcut = Shortcut.CtrlN;
fileMenu.MenuItems.Add(newFile);
openFile = new MenuItem(" & Open...");
openFile.Click += new EventHandler(MenuFileOpenOnClick);
openFile.Shortcut = Shortcut.CtrlO;
fileMenu.MenuItems.Add(openFile);
saveFile = new MenuItem(" & Save");
saveFile.Click += new EventHandler(MenuFileSaveOnClick);
saveFile.Shortcut = Shortcut.CtrlS;
fileMenu.MenuItems.Add(saveFile);
MenuItem saveAsFile = new MenuItem("Save &As...");
saveAsFile.Click += new EventHandler(MenuFileSaveAsOnClick);
fileMenu.MenuItems.Add(saveAsFile);
...

I expect this to be close enough to get a new error, and it might even work. Let s find out. The error is

 c:\data\csharp\notepad\saveload.test 
*Expected
*enter
*output
<P>uno</P>
<P>duo</P>
<P>tres</P>
*Result
<P><P>

What s this telling us? Well, first of all I don t see where that *enter is coming from. Oh ”there s one in the test. Look at it:

 *input 
<P>uno</P>
<P>duo</P>
<P>tres</P>
*end
*savefile
*input
<P><P>
*end
*output
<P><P>
*end
*loadfile
*output
*enter
*output
<P>uno</P>
<P>duo</P>
<P>tres</P>
*end

That s basically wrong. I ll remove that and try the test again:

 *input 
<P>uno</P>
<P>duo</P>
<P>tres</P>
*end
*savefile
*input
<P><P>
*end
*output
<P><P>
*end
*loadfile
*output
<P>uno</P>
<P>duo</P>
<P>tres</P>
*end

Now it says

 c:\data\csharp\notepad\saveload.test 
*Expected
<P>uno</P>
<P>duo</P>
<P>tres</P>
*Result
<P><P>

It looks like the loadfile got completely the wrong thing, or else the file never got saved. I ll look for the file next. I don t see it anywhere. Now it s definitely time for the debugger. I ll set a breakpoint in the SaveFile method and run the tests. The breakpoint fires, and we find the right menu, saveFile. But when we do the PerformClick(), nothing happens; it just returns. What s up with that? Oh. That s not so mysterious ”look at the code for that menu item:

 void MenuFileSaveOnClick(object obj, EventArgs ea) { 
}

It says to do nothing, and nothing happens. That s to be expected. Time for some reflection, however.

Lesson  

Remember that I decided to write the test rather than complete the functionality of saving and loading. My thought was that I had already seen the need for the test and that the test would help me be sure things were right. However, going down the testing path caused me to lose my train of thought on the implementation for a moment. Thus my surprise.

Some people keep a note card by their side where they make notes about what they re up to, crossing things off when they re done, looking back at the card to see what to do next. Something like that might have saved me a moment s confusion, and I suspect that a pair programmer would have helped me as well.

The real problem, however, is just that this was too big a step. When I get confused in the middle of some operation, that s a good sign that it was too big a bite. We ll press on for now, but then let s remember to see what we could have done that was a bit smaller.

For now, we ll go ahead and implement the MenuFileSaveOnClick() method. I m going to take a shortcut for now, knowing that the file name will be present. In the final implementation, we have to check whether we have a file name and run the Save As dialog if not. For now, this should do the job:

 void MenuFileSaveOnClick(object obj, EventArgs ea) { 
CallModel(saveAction);
}

I m surprised to find that that didn t work. Same message. Breakpoint again. The PerformClick() still doesn t do anything. I m confused. A little stepping and a breakpoint inside the MenuFileSaveOnClick method tells me that it is executing and in fact it is saving correctly. I m just guessing that you can t step into a PerformClick() method, possibly because they are triggering events. Anyway, the code is working so far, and it s time to make the load work also. This will be tricky. Load should unconditionally open a dialog. To make this test run correctly, I m going to have to work around that. For now, I ll just short- circuit it like I did with the save:

 class XMLNotepad : Form { 
public TestableTextBox textbox;
private TextModel model;
private MenuItem insertPre;
private MenuItem insertSection;
private MenuItem openFile;
private MenuItem saveFile;
private ModelAction enterAction;
private ModelAction shiftEnterAction;
private ModelAction insertSectionAction;
private ModelAction insertPreTagAction;
private ModelAction saveAction;
private ModelAction loadAction;
private String fileName;
...
private void InitializeDelegates(TextModel model) {
enterAction = new ModelAction(model.Enter);
shiftEnterAction = new ModelAction(model.InsertReturn);
insertSectionAction = new ModelAction(model.InsertSectionTags);
insertPreTagAction = new ModelAction(model.InsertPreTag);
saveAction = new ModelAction(this.SaveFile);
loadAction = new ModelAction(this.LoadFile);
}
...
void LoadFile() {
using ( StreamReader reader = File.OpenText(fileName) ) {
model.Load(reader);
}
}

The test still fails, but in a happy way:

 c:\data\csharp\notepad\saveload.test 
*Expected
<P>uno</P>
<P>duo</P>
<P>tres</P>
*Result
<P>uno</P>
<P>duo</P>
<P>tres</P>

The only remaining problem is that the cursor isn t set correctly. We ll fix that and be done. My plan is to fix it by changing the test for now. I don t much care where the cursor goes, and I don t have customer input on it. So we ll change the test, stop for now, and see what happens next time. Here s the changed test, which passes :

 *input 
<P>uno</P>
<P>duo</P>
<P>tres</P>
*end
*savefile
*input
<P><P>
*end
*output
<P><P>
*end
*loadfile
*output
<P>uno</P>
<P>duo</P>
<P>tres</P>
*end



Extreme Programming Adventures in C#
Javaв„ў EE 5 Tutorial, The (3rd Edition)
ISBN: 735619492
EAN: 2147483647
Year: 2006
Pages: 291

flylib.com © 2008-2017.
If you may any questions please contact us: flylib@qtcs.net