An Issue of Architecture
One of the things that make user-interface code hard to test is that many tools encourage the developer to create code in a place that is hard to get to from a testing framework. An example is Visual Basic. From version 1, Visual Basic has encouraged you to design the user interface and then add the code in the event handling methods that get created for the user-interface components. This is not good object-oriented code; it is, in fact, what is known as event-oriented code. Event-oriented code is notoriously hard to maintain, enhance, and test. On the plus side, it is very intuitive to develop and is often used in rapid application development tools.So does this mean we cannot use tools such as Visual Basic and now C# seeing as Visual Studio.NET enables you to easily develop GUI applications using the event-driven model? Not in the slightest. These tools are very powerful and increase our development capabilities. We need to learn how to use them better and then develop our software in such a way that makes it easy to enhance, maintain, and test.Ideally, you want to have a very thin user-interface layer on your application. The user interface should have a small amount of code. This has several advantages, including the following:
- It is easier to port your application to use a different user interface. Being able to support more than one interface is becoming more important as we move to developing applications that support mobile devices as well as desktop PCs and Web interfaces. It is easier to test code that is detached from the user interface. You can simulate inputs in code. The code is less dependent on the user interface and therefore easier to make cultural or language independent. Code that easier to test is more maintainable. If you need to change the behavior without changing the interface, this is easier to do.
If you develop your code test first, it forces you to build a thinner GUI layer. This GUI layer is not necessarily as thin as it could be, however, as shown in the following exercises.
Exercise 8-1: Building a Thin GUI Layer to Make Testing Easier
In the following exercise, we build a C# application that draws (stamps) shapes (circles and squares) in an area of the screen. The user can pick a shape to stamp and a color. This will emulate some children's toys that do a similar thing. The purpose of the exercise is not to create this application, but to discover how we can best write such an application to enable testing. Once again, I want us to try to do this by writing the tests first; let's see whether this is possible.
1. Create a new C# Windows application called Stamper.2. Add a reference to the NUnit.Framework.dll.3. Add a new class called StampTests.4. In the StampTests.cs file, import the Nunit.Framework namespace and set the class up as a test fixture.
Now we are ready to think about adding our first piece of functionality test first. We will start by adding the ability to draw a black square. We need a method to test that the square has been drawn.5. Create a function called TestDrawSquare in the StampTests class.
using System;
using NUnit.Framework;
namespace Stamper
{
[TestFixture]
public class StampTests
{
}
}
Many developers claim I'm crazy when they see this function! What are you doing? Testing that you've drawn a square? Well, yes, that is what I'm doing, and my level of craziness depends entirely on your point of view. If it is critical that your application draws perfect shapes, this technique could be useful for you.Hint: Use a different algorithm to test the drawing than you use for doing the drawing.6. Now we can write a class that lets us draw a square. Create a new C# class called Stamper.7. In the Stamper.cs file, add a method to the class called DrawSquare that does nothing, so we can get the project to compile. Then compile and run the test; it will fail because we haven't drawn anything yet!
using System.Drawing;
using System.Drawing.Imaging;
.
.
.
[Test]
public void TestDrawSquare()
{
Bitmap bmp = new Bitmap(50, 50,
PixelFormat.Format24bppRgb);
Graphics grph = Graphics.FromImage(bmp);
grph.Clear(Color.White);
Stamper stamp = new Stamper();
stamp.DrawSquare(new Point(10, 10), 10, grph);
Color col;
Color expectedCol;
int i;
expectedCol = Color.FromArgb(255, 0, 0, 0);
for (i = 10; i<20; i++)
{
//check top and bottom
col = bmp.GetPixel(i, 10);
Assert.AreEqual(expectedCol, col,
"Top Color incorrect");
col = bmp.GetPixel(i, 20);
Assert.AreEqual(expectedCol, col,
"Bottom Color incorrect");
//check sides
col = bmp.GetPixel(10, i);
Assert.AreEqual(expectedCol, col,
"Left Color incorrect");
col = bmp.GetPixel(20, i);
Assert.AreEqual(expectedCol, col,
"Right Color incorrect");
}
//check outsides
expectedCol = Color.FromArgb(255, 255, 255, 255);
for (i=9; i<21; i++)
{
col = bmp.GetPixel(9, i);
Assert.AreEqual(expectedCol, col,
"Left Outside Color incorrect");
col = bmp.GetPixel(21, i);
Assert.AreEqual(expectedCol, col,
"Right Outside Color incorrect");
col = bmp.GetPixel(i, 9);
Assert.AreEqual(expectedCol, col,
"Top Outside Color incorrect");
col = bmp.GetPixel(i, 21);
Assert.AreEqual(expectedCol, col,
"Bottom Outside Color incorrect");
}
}
8. Add the following two lines of code to the method and then compile and run the test again. This time the test should pass.
using System;
using System.Drawing;
namespace Stamper
{
public class Stamper
{
public void DrawSquare(Point pt, int sideLength,
Graphics grph)
{
}
}
}
We have built a class that can draw a square and have tested that it draws the edges of the square. We haven't tested that it hasn't drawn inside the square or somewhere else away from the edges. How far you want to take this level of testing depends on the nature of your application.In the next steps of the exercise, we add the capability of drawing a circle. We only test that the outside edges do not overlap and that four points of the circle all touch the edge of a square that the circle is drawn inside. This is far less rigorous than the test for the square and, in fact, the DrawSquare method would pass the test for drawing a circle, but not the other way around!9. Back in the StampTests TestFixture class, add a method to test the circle drawing functionality called TestDrawCircle.
public void DrawSquare(Point pt, int sideLength,
Graphics grph)
{
Rectangle rect = new Rectangle(pt,
new Size(sideLength, sideLength));
grph.DrawRectangle(new Pen(Color.Black), rect);
}
10. This won't compile yet because we need to add the DrawCircle method, so let's add the stub for that and then compile and run the tests.
[Test]
public void TestDrawCircle()
{
Bitmap bmp = new Bitmap(50, 50,
PixelFormat.Format24bppRgb);
Graphics grph = Graphics.FromImage(bmp);
grph.Clear(Color.White);
Stamper stamp = new Stamper();
stamp.DrawCircle(new Point(10, 10), 10, grph);
Color col;
Color expectedCol;
//check top and bottom
int i;
expectedCol = Color.FromArgb(255, 0, 0, 0);
i = 15;
col = bmp.GetPixel(i, 10);
Assert.AreEqual(expectedCol, col,
"Top Color incorrect");
col = bmp.GetPixel(i, 20);
Assert.AreEqual(expectedCol, col,
"Bottom Color incorrect");
//check sides
col = bmp.GetPixel(10, i);
Assert.AreEqual(expectedCol, col,
"Left Color incorrect");
col = bmp.GetPixel(20, i);
Assert.AreEqual(expectedCol, col,
"Right Color incorrect");
//check outsides
expectedCol = Color.FromArgb(255, 255, 255, 255);
for(i=9; i<21; i++)
{
col = bmp.GetPixel(9, i);
Assert.AreEqual(expectedCol, col,
"Left Outside Color incorrect");
col = bmp.GetPixel(21, i);
Assert.AreEqual(expectedCol, col,
"Right Outside Color incorrect");
col = bmp.GetPixel(i, 9);
Assert.AreEqual(expectedCol, col,
"Top Outside Color incorrect");
col = bmp.GetPixel(i, 21);
Assert.AreEqual(expectedCol, col,
"Bottom Outside Color incorrect");
}
}
11. The TestDrawCircle failed, so let's put the code in to make it pass.
public void DrawCircle(Point pt ,int diameter,
Graphics grph)
{
}
12. The next thing to do is add the color functionality; we need to be able to draw squares or circles in red or black. So far our tests have just assumed they would be black. We can modify our tests to be more explicit about the fact they test for the black shapes, by changing the method names and setting a (yet to be created) color property on the Stamper object to black.
public void DrawCircle(Point pt ,int diameter,
Graphics grph)
{
Rectangle rect = new Rectangle(pt,
new Size(diameter, diameter));
grph.DrawEllipse(new Pen(Color.Black), rect);
}
13. We must now add this new color property to the Stamper class. Notice we have hard coded the property to Black, and we are ignoring the set method. This is the simplest thing to do that will make all the tests run and not break any existing functionality. We know this will change in the next few steps when we add the capability for the shapes to be drawn in red.
[Test]
public void TestDrawBlackSquare()
{
Bitmap bmp = new Bitmap(50, 50,
PixelFormat.Format24bppRgb);
Graphics grph = Graphics.FromImage(bmp);
grph.Clear(Color.White);
Stamper stamp = new Stamper();
stamp.Color = Color.Black;
stamp.DrawSquare(new Point(10, 10), 10, grph);
Color col;
Color expectedCol;
.
.
.
}
[Test]
public void TestDrawBlackCircle()
{
Bitmap bmp = new Bitmap(50, 50,
PixelFormat.Format24bppRgb);
Graphics grph = Graphics.FromImage(bmp);
grph.Clear(Color.White);
Stamper stamp = new Stamper();
stamp.Color = Color.Black;
stamp.DrawCircle(new Point(10, 10), 10, grph);
Color col;
Color expectedCol;
.
.
.
}
14. Compile and run the tests.15. We now can add the functionality for drawing the shapes in red. We start by adding a new test called TestDrawRedSquare. This is very similar to the test for the black square, so you can copy and paste that method and then make the following changes:
public Color Color
{
get{return Color.Black;}
set{ }
}
16. The project should compile, and the test will fail. To make the test pass, we need to do some work in the Stamper class to actually use the color that is exposed as a property. First we create a member variable of the class of type Pen and use it to store (and set) the color property that is exposed from the class.
[Test]
public void TestDrawRedSquare()
{
Bitmap bmp = new Bitmap(50, 50,
PixelFormat.Format24bppRgb);
Graphics grph = Graphics.FromImage(bmp);
grph.Clear(Color.White);
Stamper stamp = new Stamper();
stamp.Color = Color.Red;
stamp.DrawSquare(new Point(10, 10), 10, grph);
Color col;
Color expectedCol;
int i;
expectedCol = Color.FromArgb(255, 255, 0, 0);
for (i = 10; i<20; i++)
{
.
.
.
17. We can now use this _pen object in our Draw methods.
private Pen _pen = new Pen(Color.Black);
public Color Color
{
get{return _pen.Color;}
set{ _pen.Color= value; }
}
18. Compile and run the tests; they should pass. You should now add a test for drawing red circles as well.19. So far we have not added a single thing to the real user interface. If you run the program, you will see a blank form that doesn't do much. We should now think about connecting the Stamper class we have built to some form of user interface. Open the form in design view and add a Picture Box control (leave it as PictureBox1), two labels (called Square and Circle), and two panels (called Red and Black). Set the background color property on the panels to the same color as the names; you can change the background color of the label controls and set the text to the same as their names (see Figure 8-1).
public void DrawSquare(Point pt, int sideLength,
Graphics grph)
{
Rectangle rect = new Rectangle(pt,
new Size(sideLength, sideLength));
grph.DrawRectangle(_pen, rect);
}
public void DrawCircle(Point pt ,int diameter,
Graphics grph)
{
Rectangle rect = new Rectangle(pt,
new Size(diameter, diameter));
grph.DrawEllipse(_pen, rect);
}
Figure 8-1. The user interface.
[View full size image]

21. Create an event handler for the form load event; you can do this by doubleclicking the form in the design view. Edit the code to set the stamper color to black by default.
public class Form1 : System.Windows.Forms.Form
{
private Stamper stamp = new Stamper();
22. Next we want to add code to draw a square in the PictureBox when the user clicks the mouse button while the cursor is over the PictureBox. To add an event handler for the MouseDown event, go back to the design view and select the PictureBox. Then from the Properties window, select Events and double-click the MouseDown event. This will create the event handler function for us. We can now fill in the skeleton provided with code to call the Stamper and place a square in the picture box.
using System.Drawing.Imaging;
.
.
.
private void Form1_Load(object sender, System.EventArgs e)
{
stamp.Color = Color.Black;
pictureBox1.Image = new Bitmap(pictureBox1.Width,
pictureBox1.Height,
PixelFormat.Format24bppRgb);
Graphics grph = Graphics.FromImage(pictureBox1.Image);
grph.Clear(Color.White);
}
23. You can compile and run the program; you can draw squares in the picture box!24. We will now add color functionality to the program. Back in the design view, select the Red panel and then in the Properties window double-click the Click event to create an event handler for the Mouse Click on the Red Panel. Edit the code to change the color of the Stamper to red.
private void pictureBox1_MouseDown(object sender,
System.Windows.Forms.MouseEventArgs e)
{
Point pt = new Point(e.X, e.Y);
Bitmap bmp = pictureBox1.Image as Bitmap;
Graphics grph = Graphics.FromImage(bmp);
stamp.DrawSquare(pt, 10, grph);
pictureBox1.Refresh();
}
25. You can do the same for the black panel on the click event, setting the color to black and changing the border style to reflect which color is selected. Then compile and run the program. You should now be able to draw red or black squares.26. The last thing to do is add the capability to draw either squares or circles. We start by defining a new type to represent the shape we want to draw. In the Form1 class, add a new enum and a member of the class to store the shape.
private void Red_Click(object sender, System.EventArgs e)
{
Red.BorderStyle = BorderStyle.Fixed3D;
Black.BorderStyle = BorderStyle.None;
stamp.Color = Color.Red;
}
27. Add event handler methods for the Square and Circle Click events.
enum Shape
{
Square,
Circle
}
private Shape stampShape = Shape.Square;
28. Finally, we need to use the shape in the PictureBox MouseDown event handler method.
private void Square_Click(object sender, System.EventArgs e)
{
Square.BorderStyle = BorderStyle.Fixed3D;
Circle.BorderStyle = BorderStyle.None;
stampShape = Shape.Square;
}
private void Circle_Click(object sender, System.EventArgs e)
{
Circle.BorderStyle = BorderStyle.Fixed3D;
Square.BorderStyle = BorderStyle.None;
stampShape = Shape.Circle;
}
29. Compile and run the program. You can draw circles or squares in red or black, how wonderful.
private void pictureBox1_MouseDown(object sender,
System.Windows.Forms.MouseEventArgs e)
{
Point pt = new Point(e.X, e.Y);
Bitmap bmp = pictureBox1.Image as Bitmap;
Graphics grph = Graphics.FromImage(bmp);
if (stampShape == Shape.Circle )
stamp.DrawCircle(pt, 10, grph);
else
stamp.DrawSquare(pt, 10, grph);
pictureBox1.Refresh();
}
Figure 8-2. Stamping circles and squares in red and black.
[View full size image]

Stamper Part Two
In this exercise, we need to add the functionality to stamp out triangles as well as the circles and squares that the program already does. Use your knowledge of refactoring to refactor the Stamper program and add the triangle functionality.
Stamper Part Three
Carrying on with the Stamper program, another software team now needs to use the Stamper functionality in their application. Take what you have worked on in Exercise 8-1 and build a Windows control with the Stamper functionality. If your refactoring of the Stamper was done well, this exercise should be fairly easy. (Hint: This control should expose the color and shape of the stamps as properties.)
Exercise 8-2: Using Reflection to Test the GUI
The preceding section discussed architecting the application to make the GUI layer thinner and therefore easier to test. We didn't actually test the GUI controls such as the buttons or panels. In the following exercise, we develop a Windows Forms application by writing the tests first. This exercise shows you how to use reflection to test the user-interface components on a Windows form. The application we will develop will itself use reflection to display the methods, properties, and fields (variables) of classes in an assembly.Let's start off with creating the form.
1. Create a new C# Windows Application project called GUITest.2. Add a reference to the NUnit.Framework.dll in the Project. (Right-click the References folder in Solution Explorer and select Add Reference.)3. Add a new C# class to the project called GUITests.cs and edit the code in the class file to look like this.
Notice we have added a using reference to System.Windows.Forms and System.Reflection; you will see why shortly.4. The next thing to do is put some tests into the TestFixture we have just created. "Hold on," I can hear you thinking, "we haven't even added anything to the form!" Please bear with me and add the following code, and then let's see what we have done.
using System;
using NUnit.Framework;
using System.Windows.Forms;
using System.Reflection;
namespace GUITest
{
[TestFixture]
public class GUITests
{
}
}
We have added a setup and a teardown method. These methods run before and after each test method is run. They create and show the form, and then close and dispose of the form, respectively.In our first test, we are getting down to business. We are using reflection to first get a FieldInfo class for a TextBox member variable of the form called AssemblyName. We then set the value in the text box to the path of this application. Next we get information about a method called LoadAssembly_Click (which I plan to be fired when a button is clicked) and invoke that method. Finally, we get details of a Label control that is a member of the form and assert that the text on the label is equal to GUITest.We have done all this without adding any controls to our form, and yet this application will compile and run. If you run the test now in Nunit, you will notice the form flash up, although the test fails, obviously.Deepak and Eddie from our eXtreme .NET team had a conversation about the test-first approach to develop GUI applications.
[TestFixture]
public class GUITests
{
Form1 testForm;
BindingFlags flags = BindingFlags.NonPublic|
BindingFlags.Public|BindingFlags.Static|
BindingFlags.Instance;
Type tForm = typeof(Form1);
[SetUp]
public void SetupForm()
{
testForm = new Form1();
testForm.Show();
}
[TearDown]
public void TearDownForm()
{
testForm.Close();
testForm.Dispose();
}
[Test]
public void TestLoadAssembly()
{
FieldInfo textBoxInfo =
tForm.GetField("AssemblyEntered",flags);
TextBox textBox =
(TextBox)textBoxInfo.GetValue(testForm);
textBox.Text =
@"C:\Work\GUITest\bin\Debug\GUITest.exe";
MethodInfo clickMethod =
tForm.GetMethod("LoadAssembly_Click",flags);
Object[] args = new Object[2];
args[0] = this;
args[1] = new EventArgs();
clickMethod.Invoke(testForm, args);
FieldInfo labelInfo =
tForm.GetField("LoadedAssembly",flags);
Label label = (Label)labelInfo.GetValue(testForm);
string strText = label.Text;
Assert.AreEqual(
@"file:///C:/Work/GUITest/bin/Debug/GUITest.EXE",
strText,
"Assembly Name Incorrect");
}
}

Figure 8-3. Add a button, label, and text box.

7. Edit the code for the LoadAssembly_Click method as shown here.
using System.Reflection;
8. Compile the program and run the test. It should pass. If you are quick (or have a slow machine), you will notice the form load up briefly on the screen. You have now built a user interface and tested it without even having to run the program! You can run the program if you want and load up an assembly, but it doesn't do much yet.To add the rest of the functionality, we will add some radio buttons to select what we want to display and a TreeView control in which to display the methods, properties, and fields for each of the classes in the assembly.9. We'll start by writing a test to validate that the TreeView (that we haven't created yet) is correctly displaying the classes for the assembly that is loaded. We know about the classes in this assembly we are building. We will use these classes to test the TreeView. In the GUITests class, add a new method called ValidateClassesInTreeView.
private void LoadAssembly_Click(object sender, System.EventArgs e)
{
try
{
string strApplication = AssemblyEntered.Text;
AssemblyName aName =
AssemblyName.GetAssemblyName(strApplication);
Assembly assembly = Assembly.Load(aName);
LoadedAssembly.Text = assembly.CodeBase;
}
catch(Exception)
{
LoadedAssembly.Text = "Error Loading Assembly";
}
}
10. Compile and run the tests; this one should fail. Note: We have some duplicate code in the two tests. We'll come back and refactor that code soon.11. We need to add the TreeView to the form and fill in the code necessary to make the tests pass. In the design view of the form, drag a TreeView control onto the form and rename it AssemblyTypesTree (as specified in the test above). In the LoadAssembly_Click method, add the following code to display the loaded assembly's types.
[Test]
public void ValidateClassesInTreeView()
{
FieldInfo textBoxInfo =
tForm.GetField("AssemblyEntered",flags);
TextBox textBox =
(TextBox)textBoxInfo.GetValue(testForm);
textBox.Text =
@"C:\Work\GUITest\bin\Debug\GUITest.exe";
MethodInfo clickMethod =
tForm.GetMethod("LoadAssembly_Click",flags);
Object[] args = new Object[2];
args[0] = this;
args[1] = new EventArgs();
clickMethod.Invoke(testForm, args);
FieldInfo treeViewInfo =
tForm.GetField("AssemblyTypesTree",flags);
TreeView treeView =
(TreeView)treeViewInfo.GetValue(testForm);
Assert.AreEqual("Form1", treeView.Nodes[0].Text,
"Incorrect Node(0) in Tree");
Assert.AreEqual("GUITests", treeView.Nodes[1].Text,
"Incorrect Node(1) in Tree");
}
12. Compile and run the tests. They should pass and once again we haven't yet run the application. Now run the application to check that the TreeView is displaying the classes. You will need to enter the path for the assembly in the text box and click the LoadAssembly button. You should then see the classes for that assembly shown (see Figure 8-4).
private void LoadAssembly_Click(object sender, System.EventArgs e)
{
try
{
string strApplication = AssemblyEntered.Text;
AssemblyName aName =
AssemblyName.GetAssemblyName(strApplication);
Assembly assembly = Assembly.Load(aName);
LoadedAssembly.Text = assembly.CodeBase;
Type[] aTypes = assembly.GetTypes();
AssemblyTypesTree.Nodes.Clear();
foreach(Type aType in aTypes)
{
AssemblyTypesTree.Nodes.Add(aType.Name);
}
}
catch(Exception)
{
LoadedAssembly.Text = "Error Loading Assembly";
}
}
Figure 8-4. See the types in an assembly.

14. Make sure the code still compiles and the tests run and pass as before.15. Now let's add the radio buttons to select whether to show the methods, properties, or fields of the classes. Staying with the GUITest class, we'll add a method to test each of the button selections.
public void LoadGUITestAssembly()
{
FieldInfo textBoxInfo =
tForm.GetField("AssemblyEntered",flags);
TextBox textBox =
(TextBox)textBoxInfo.GetValue(testForm);
textBox.Text =
@"C:\Work\GUITest\bin\Debug\GUITest.exe";
MethodInfo clickMethod =
tForm.GetMethod("LoadAssembly_Click",flags);
Object[] args = new Object[2];
args[0] = this;
args[1] = new EventArgs();
clickMethod.Invoke(testForm, args);
}
[Test]
public void TestLoadAssembly()
{
LoadGUITestAssembly();
FieldInfo labelInfo =
tForm.GetField("LoadedAssembly",flags);
Label label = (Label)labelInfo.GetValue(testForm);
string strText = label.Text;
Assert.AreEqual(
@"file:///C:/Work/GUITest/bin/Debug/GUITest.EXE",
strText,
"Assembly Name Incorrect");
}
[Test]
public void ValidateClassesInTreeView()
{
LoadGUITestAssembly();
FieldInfo treeViewInfo =
tForm.GetField("AssemblyTypesTree",flags);
TreeView treeView =
(TreeView)treeViewInfo.GetValue(testForm);
Assert.AreEqual("Form1", treeView.Nodes[0].Text,
"Incorrect Node(0) in Tree");
Assert.AreEqual("GUITests", treeView.Nodes[1].Text,
"Incorrect Node(1) in Tree");
}
16. Run the tests in NUnit; these new ones should all fail.17. We can now add the radio buttons and the code to make the tests pass. In the design view for the form, add three radio buttons next to the TreeView control. Name them Methods, Properties and Fields. Change the text on the radio buttons to also reflect the names. Then for each radio button generate a method for the click event. You can do this in the Properties window; just click the Events button and double-click the Click event. The methods should be named (automatically) Methods_Click, Properties_Click and Fields_Click.18. To fill in the code for the radio button click events, we need access to the assembly that is loaded. To have this access, we need to extract the Assembly variable out of the LoadAssembly_Click method and make it a variable scoped by the class.
[Test]
public void ValidateMethodsInTreeView()
{
LoadGUITestAssembly();
FieldInfo treeViewInfo =
tForm.GetField("AssemblyTypesTree",flags);
TreeView treeView =
(TreeView)treeViewInfo.GetValue(testForm);
MethodInfo clickMethod =
tForm.GetMethod("Methods_Click",flags);
Object[] args = new Object[2];
args[0] = this;
args[1] = new EventArgs();
clickMethod.Invoke(testForm, args);
TreeNodeCollection classMethods =
treeView.Nodes[0].Nodes;
Assert.AreEqual("OnMenuComplete",
classMethods[1].Text,
"Incorrect Method in Tree");
}
[Test]
public void ValidatePropertiesInTreeView()
{
LoadGUITestAssembly();
FieldInfo treeViewInfo =
tForm.GetField("AssemblyTypesTree",flags);
TreeView treeView =
(TreeView)treeViewInfo.GetValue(testForm);
MethodInfo clickMethod =
tForm.GetMethod("Properties_Click",flags);
Object[] args = new Object[2];
args[0] = this;
args[1] = new EventArgs();
clickMethod.Invoke(testForm, args);
TreeNodeCollection classProps =
treeView.Nodes[0].Nodes;
Assert.AreEqual("ActiveMdiChild",
classProps[1].Text,
"Incorrect Properties in in Tree");
}
[Test]
public void ValidateFieldsInTreeView()
{
LoadGUITestAssembly();
FieldInfo treeViewInfo =
tForm.GetField("AssemblyTypesTree",flags);
TreeView treeView =
(TreeView)treeViewInfo.GetValue(testForm);
MethodInfo clickMethod =
tForm.GetMethod("Fields_Click",flags);
Object[] args = new Object[2];
args[0] = this;
args[1] = new EventArgs();
clickMethod.Invoke(testForm, args);
TreeNodeCollection classFields =
treeView.Nodes[0].Nodes;
Assert.AreEqual("AssemblyEntered",
classFields[1].Text,
"Incorrect Field in in Tree");
}
19. Note the tests are testing the value of the fields, methods, and properties of the Form1 class in this application. The order of declaration will be important. Compile the program and run the tests; they should all pass. If they don't, check that the order of your declarations matches what we are testing. For example, in the test ValidateFieldsInTreeView, we are testing that AssemblyEntered is the second field declared in the class. If this is not the case, the test will fail. To ensure it passes, change the order of declaration in your Form1 class file.20. Now run the program and see whether you can break it. There is one obvious way. Load the application, and then, without loading an assembly, click one of the radio buttons. This causes an issue because the assembly variable has not yet been set. Let's fix this "test first."21. Create a new method in the GUITests class called TestMethodsInTreeWithInvalidAssembly. This method will call the click method in the form before loading an assembly.
private Assembly assembly;
private BindingFlags flags =
BindingFlags.NonPublic|BindingFlags.Public|
BindingFlags.Static|BindingFlags.Instance;
private void LoadAssembly_Click(object sender, System.EventArgs e)
{
try
{
string strApplication = AssemblyEntered.Text;
AssemblyName aName =
AssemblyName.GetAssemblyName(strApplication);
assembly = Assembly.Load(aName);
LoadedAssembly.Text = assembly.CodeBase;
Type[] aTypes = assembly.GetTypes();
AssemblyTypesTree.Nodes.Clear();
foreach(Type aType in aTypes)
{
AssemblyTypesTree.Nodes.Add(aType.Name);
}
}
catch(Exception)
{
LoadedAssembly.Text = "Error Loading Assembly";
}
}
private void Methods_Click(object sender, System.EventArgs e)
{
Type[] aTypes = assembly.GetTypes();
AssemblyTypesTree.Nodes.Clear();
foreach(Type aType in aTypes)
{
TreeNode node =
AssemblyTypesTree.Nodes.Add(aType.Name);
MethodInfo[] methods = aType.GetMethods(flags);
foreach(MethodInfo method in methods)
{
node.Nodes.Add(method.Name);
}
}
}
private void Properties_Click(object sender, System.EventArgs e)
{
Type[] aTypes = assembly.GetTypes();
AssemblyTypesTree.Nodes.Clear();
foreach(Type aType in aTypes)
{
TreeNode node =
AssemblyTypesTree.Nodes.Add(aType.Name);
PropertyInfo[] props = aType.GetProperties(flags);
foreach(PropertyInfo prop in props)
{
node.Nodes.Add(prop.Name);
}
}
}
private void Fields_Click(object sender, System.EventArgs e)
{
Type[] aTypes = assembly.GetTypes();
AssemblyTypesTree.Nodes.Clear();
foreach(Type aType in aTypes)
{
TreeNode node =
AssemblyTypesTree.Nodes.Add(aType.Name);
FieldInfo[] fields = aType.GetFields(flags);
foreach(FieldInfo field in fields)
{
node.Nodes.Add(field.Name);
}
}
}
22. Compile the program and run the test. It will fail, throwing an exception. We must now fix the code.23. In the Methods_Click method, add the following highlighted lines to return when the assembly is not valid.
[Test]
public void TestMethodsInTreeWithInvalidAssembly()
{
FieldInfo treeViewInfo =
tForm.GetField("AssemblyTypesTree",flags);
TreeView treeView =
(TreeView)treeViewInfo.GetValue(testForm);
MethodInfo clickMethod =
tForm.GetMethod("Methods_Click",flags);
Object[] args = new Object[2];
args[0] = this;
args[1] = new EventArgs();
clickMethod.Invoke(testForm, args);
Assert.AreEqual(0, treeView.Nodes.Count,
"Tree contains nodes when no assembly is loaded");
}
24. Compile and run the tests again. They should all pass. I leave it for you to do the same for each of the other radio button click methods.25. A lot of duplicate code is scattered around this little application. It should be refactored to remove duplication. This you can do on your own to test your refactoring skills. Remember to run the tests after every change to validate you have not broken anything.This exercise has shown you how it is possible to develop user-interface code using the principles of test-driven design. By developing your code in a test-first manner, you need to think more about what you are going to call the controls before you drag them from the toolbox onto the form. I believe this is a good thing because you will be more likely to give meaningful names to the controls and their methods.
private void Methods_Click(object sender, System.EventArgs e)
{
if (null == assembly)
{
Methods.Checked = false;
return;
}
Type[] aTypes = assembly.GetTypes();
AssemblyTypesTree.Nodes.Clear();
foreach(Type aType in aTypes)
{
TreeNode node =
AssemblyTypesTree.Nodes.Add(aType.Name);
MethodInfo[] methods = aType.GetMethods(flags);
foreach(MethodInfo method in methods)
{
node.Nodes.Add(method.Name);
}
}
}
Exercises on Your Own
The following two exercises are for you to carry out on your own (or with a friend); they will help you to reinforce the techniques you have learned in this chapter.
Exercise 8-3: Building a Small System
Build a Windows application that, as in Exercise 8-2, uses reflection to show the methods, properties, and fields of classes in an assembly. But use a thinner GUI layer. Try to encapsulate as much of the functionality as possible into separate classes. Of course, use the test-driven development ideas you have learned along with refactoring.This exercise should take you no more than an hour.
Exercise 8-4: Changing the GUI with Confidence
Now change the user interface on the Windows application you just built to operate from the command line.(Optional extra) Output the results into an XML file.This exercise should take no longer than 30 minutes.