Chapter 8. More Testing

This chapter focuses again on testing. Testing is one of the cornerstones of XP development. We have seen how the tasks we created in Chapter 3 drive the tests. These tests communicate the tasks being carried out. We learned how the tests support the refactoring techniques we learned in Chapter 5. The refactoring aims to simplify the code. In Chapter 6, we discovered how we can encode new knowledge into tests. These tests validate whether this knowledge is still correct each time they are run. The tests communicate the learned knowledge. In Chapter 7, we discovered how the tests running repeatedly on a build or integration machine provide the team with visible feedback as to the state of the system. The tests support three values we learned were important in Chapter 1: communication, simplicity, and feedback. Because testing is so important, this chapter returns to it again to explain some extra techniques we can use for developing our tests.

User-Interface Testing

An excuse I often hear in my line of work is this: “It is not possible to use the practices because we build user-interface code.” This chapter dispels that myth by introducing you to techniques to develop better user-interface code. These techniques enable you to test your user-interface code. I include additional ideas about test-driven development as well as cover some refactoring techniques.

It’s Not Possible!

It seems like a human trait to find an excuse why something is not possible and therefore cannot be applied. This seems especially true when people do not see the reasoning behind doing something. Many of the XP techniques at first don’t seem to add up to a sensible option for developing software. I have found the only way to prove whether something makes sense is to try it. After I have tried something, I can make a judgment as to its validity for the software I am developing. Reserve your belief that it is not possible until you have tried some of the exercises in this chapter. Then see whether you can see how to apply these ideas to your everyday project.

“We Are a Special Case”

“We are a special case” is another of the common cries I hear as an excuse as to why it is not possible to develop testable user-interface code. In my experience, most development teams are doing something reasonably special and different; that’s why they are getting paid. If most teams are doing something different, does that mean they are all special cases?

The trick is to work out how to use a best practice to your advantage in the “special case” code you are developing. Let’s examine some of the issues with user interfaces and then try some exercises. Then this chapter shows you how to write software with user interfaces that is more testable and therefore gives you more confidence in its correctness.

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.

    using System;
    using NUnit.Framework;
    
    namespace Stamper
    {
        [TestFixture]
        public class StampTests
        {
        }
    }
    

    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.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");
               }
           }
    

    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;
    using System.Drawing;
    
    namespace Stamper
    {
          public class Stamper
          {
            public void DrawSquare(Point pt, int sideLength,
                Graphics grph)
            {
            }
          }
    }
    
  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.

    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);
    }
    

    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.

    [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");
        }
    }
    
  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.

    public void DrawCircle(Point pt ,int diameter,
        Graphics grph)
    {
    }
    
  11. The TestDrawCircle failed, so let’s put the code in to make it pass.

    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);
    }
    
  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.

    [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;
        .
        .
        .
    }
    
  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.

    public Color Color
    {
        get{return Color.Black;}
        set{   }
    }
    
  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:

    [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++)
    {
    .
    .
    .
    
  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.

    private Pen _pen = new Pen(Color.Black);
    public Color Color
    {
        get{return _pen.Color;}
        set{ _pen.Color= value; }
    }
    
  17. We can now use this _pen object in our Draw methods.

    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);
    }
    
  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).

    The user interface.

    Figure 8-1. The user interface.

  20. In the code file for the form, add a private member variable called stamp to hold an instance of the Stamper class.

    public class Form1 : System.Windows.Forms.Form
    {
        private Stamper stamp = new Stamper();
    
  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.

    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);
           }
    
  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.

    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();
    }
    
  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 Red_Click(object sender, System.EventArgs e)
    {
        Red.BorderStyle = BorderStyle.Fixed3D;
        Black.BorderStyle = BorderStyle.None;
        stamp.Color = Color.Red;
    }
    
  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.

    enum Shape
    {
       Square,
       Circle
    }
    private Shape stampShape = Shape.Square;
    
  27. Add event handler methods for the Square and Circle Click events.

    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;
    }
    
  28. Finally, we need to use the shape in the PictureBox MouseDown event handler method.

    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();
    }
    
  29. Compile and run the program. You can draw circles or squares in red or black, how wonderful.

    Stamping circles and squares in red and black.

    Figure 8-2. Stamping circles and squares in red and black.

    I hope you can see that the use of tests to drive the development forced us to develop the core functionality before we connected it through to the GUI layer. This program is far from perfect, and there is some obvious refactoring that can be done to make the GUI layer even thinner.

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.

    using System;
    using NUnit.Framework;
    using System.Windows.Forms;
    using System.Reflection;
    
    namespace GUITest
    {
        [TestFixture]
        public class GUITests
        {
        }
    }
    

    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.

    [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:WorkGUITestinDebugGUITest.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");
        }
    }
    

    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.

    Let’s get the test running and hopefully you will see what Eddie is talking about.

  5. In the design view for the form, add a button, label, and text box called LoadAssembly, LoadedAssembly, and AssemblyEntered, respectively (see Figure 8-3).

    Add a button, label, and text box.

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

  6. Double-click the LoadAssembly button to create the LoadAssembly_Click method that we invoked from the test code. At the top of the Form1.cs file, add the System.Reflection reference to the using list.

    using System.Reflection;
    
  7. Edit the code for the LoadAssembly_Click method as shown here.

    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";
        }
    }
    
  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.

    [Test]
    public void ValidateClassesInTreeView()
    {
        FieldInfo textBoxInfo =
            tForm.GetField("AssemblyEntered",flags);
        TextBox textBox =
             (TextBox)textBoxInfo.GetValue(testForm);
        textBox.Text =
             @"C:WorkGUITestinDebugGUITest.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");
    }
    
  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.

    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";
        }
    }
    
  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).

    See the types in an assembly.

    Figure 8-4. See the types in an assembly.

  13. Now we can refactor some of the test code that we spotted was duplicated earlier. We will use Extract method (discussed in Chapter 5) to place the duplicate code into a single method that both the other methods can call. These methods in the GUITest class should then look like this.

    public void LoadGUITestAssembly()
    {
        FieldInfo textBoxInfo =
            tForm.GetField("AssemblyEntered",flags);
        TextBox textBox =
            (TextBox)textBoxInfo.GetValue(testForm);
        textBox.Text =
            @"C:WorkGUITestinDebugGUITest.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");
    }
    
  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.

    [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");
    }
    
  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.

    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);
             }
         }
    }
    
  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.

    [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");
    }
    
  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.

    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);
            }
        }
    }
    
  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.

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.

Testing Third-Party Libraries

Many developers are now using third-party libraries. The .NET Framework development environment encourages this by promoting the development of component-based solutions across the enterprise. When using third-party libraries, there are approaches that can be taken to maximize the quality of the code produced and reduce the cost of change through the lifetime of the system. In the following exercises, we explore how these approaches fit with the XP practices.

We All Do It

One of the classic mistakes that development teams often make is to assume that when they get a third-party library it will just work. I am not sure why this belief exists; maybe they believe the marketing hype, or the sales pitch was really good.

The flip side to this is that some developers refuse to touch third-party libraries. These developers are convinced they will definitely not work, in part, because they don’t trust any code they didn’t write themselves.

Our eXtreme .NET team faces this issue.

These misconceptions can be overcome by writing tests around the library, or at least the functionality you need to use. Because you are reading this book, I bet you are thinking of using some third-party development library. How can I be so certain?

The .NET Framework Is a Third-Party Library!

For most of you, the .NET Framework is a third-party library. If you are smart, you will also be highly likely you are using libraries, components, or code developed by other teams. This code might have been developed either in your company or other companies. Why waste your time developing code that someone else has already invested their time and effort into?

Component-Based Software Development Is Here

Microsoft has been working hard on the .NET Framework, and it provides solutions to many of the failings of their previous component-based offering, COM. For many developers, COM was good enough for their component-based applications, but there were issues with scalability to very large systems and cross-platform interoperability. The .NET Framework goes a long way toward solving these problems and opens the doors wider for component-based solutions, leveraging remoting, Web services, and cross-language library integration.

If It Goes Wrong, We’re All in the Brown Stuff

The trouble with the increase in utilization of components written by other teams of developers is that we are exposing ourselves to ever-greater unknowns. This is often why developers don’t trust anything unless they write it themselves. This lack of trust is not only arrogant, but also commercially unviable. If we are going to use more code that we don’t have “control” over, we need to find some way to protect ourselves against issues with that code. We are going to investigate how we can use some of the test-driven development practices to protect ourselves from issues arising through using functionality provided by third-party components.

Put the Alarms in Place

By writing tests for each unit of functionality that you use from a third party, you are doing three things:

  1. You are validating your theories as to how the library works.

  2. You are building confidence in the use of that library.

  3. You are protecting yourself from changes in future versions of the library.

This final one is important. If you have a test suite for a third-party library that tests all the functionality you use from that library, when a new version of the library becomes available, you can run the test suite against that new version. The tests will show you whether the changes will have a detrimental effect on the software you have written.

Step-by-Step Exercises Using a Third-Party Library

In the next exercise, we learn how to build a test suite for a .NET Framework class library in C#. The class library provides some simple functionality for loading and saving collections of time zone information. This library will be useful for the clocks application we first encountered in Chapter 3. This library could have been written by a colleague at work, another department, an external contractor, or bought off the shelf or downloaded from a Web site. Its origins are not important; validation of its functionality is what we care about.

Let’s code.

Exercise 8-5: Setting Up NUnit (Again!)

  1. Create a new C# console application in Visual Studio called TimeZones.

  2. As in the previous exercises, add a reference to the NUnit.Framework.dll.

  3. Also add a reference to the TimeZoneSerializer.dll (which you can download from http://eXtreme.NET.roodyn.com/BookExercises.aspx).

  4. Create a new class called TestTimeZoneSerializer.cs and edit the file, creating a class called TestTimeZoneSerializer, using the TestFixture attribute to mark it as a fixture.

    using System;
    using NUnit.Framework;
    
    namespace TimeZones
    {
        [TestFixture]
        public class TestTimeZoneSerializer
        {
    
        }
    }
    

We now have a C# console application that has a testing framework ready to test the library we want to use.

Exercise 8-6: The Quick Breadth Test

The first thing we can test is an overall breadth of the functionality we require. If this library doesn’t do what we need or operate adequately, we want to know as soon as possible. There is no point in writing detailed tests for each piece of functionality that all succeed, only to find at the end that the overall behavior doesn’t match our expectations. Writing a couple of overall breadth of functionality tests first will let us know very quickly whether it is worth continuing and writing some more detailed tests for the functions being used.

  1. To test the TimeZoneSerializer, we can make our lives easier by including the namespace in our using list. We also need the System.Globalization namespace to access DateTimeFormatInfo. Add them to the top of the test class file.

    using TimeZoneSerializer;
    using System.Globalization;
    
  2. Create a new Test method called BreadthTest. In this method, we create a time zones file, add some zones to it, and save it. We then load the file and ensure that it contains the zones we added. This will give us the confidence that the overall functionality is roughly working and does what we need.

    [Test]
    public void BreadthTest()
    {
        TimeZonesFile zoneDoc = new TimeZonesFile();
    
        TimeZoneData zoneLondon = new TimeZoneData();
        zoneLondon.m_strPlace = "London";
        zoneLondon.m_nBias                  = 0;
        zoneLondon.m_nDaylightBias      = -60;
        zoneLondon.m_nStandardBias      = 0;
        zoneLondon.m_DaylightDate = DateTime.Parse(
            "03/31/2002 01:00",
            DateTimeFormatInfo.InvariantInfo);
        zoneLondon.m_StandardDate = DateTime.Parse(
            "10/27/2002 02:00",
            DateTimeFormatInfo.InvariantInfo);
        zoneDoc.AddZone(zoneLondon);
    
        TimeZoneData zoneSydney = new TimeZoneData();
        zoneSydney.m_strPlace ="Sydney";
        zoneSydney.m_nBias                  =-600;
        zoneSydney.m_nDaylightBias      =-60;
        zoneSydney.m_nStandardBias      =0;
        zoneSydney.m_DaylightDate = DateTime.Parse(
            "10/27/2002 02:00",
            DateTimeFormatInfo.InvariantInfo);
        zoneSydney.m_StandardDate = DateTime.Parse(
            "03/31/2002 03:00",
            DateTimeFormatInfo.InvariantInfo);
        zoneDoc.AddZone(zoneSydney);
    
        TimeZoneData zoneLA = new TimeZoneData();
        zoneLA.m_strPlace = "L.A.";
        zoneLA.m_nBias                 =480;
        zoneLA.m_nDaylightBias     =-60;
        zoneLA.m_nStandardBias     =0;
        zoneLA.m_DaylightDate = DateTime.Parse(
            "04/07/2002 02:00",
            DateTimeFormatInfo.InvariantInfo);
        zoneLA.m_StandardDate = DateTime.Parse(
            "10/27/2002 02:00",
            DateTimeFormatInfo.InvariantInfo);
        zoneDoc.AddZone(zoneLA);
        zoneDoc.WriteToFile(
            @"C:TestTimeZoneSerializer.TestBreadth.xml");
    
        TimeZonesFile loadedZones = TimeZonesFile.LoadFromFile(
            @"C:TestTimeZoneSerializer.TestBreadth.xml");
        int zonesLoaded = 0;
        foreach (TimeZoneData tzone in loadedZones.Zones)
        {
            switch (tzone.m_strPlace)
            {
                case "London":
                    Assert.AreEqual(zoneLondon, tzone,
                        "London zone is not same as saved");
                    zonesLoaded ++;
                    break;
                case "Sydney":
                    Assert.AreEqual(zoneSydney, tzone,
                        "Sydney zone is not same as saved");
                    zonesLoaded ++;
                    break;
                case "L.A.":
                    Assert.AreEqual(zoneLA, tzone,
                        "L.A. zone is not same as saved");
                    zonesLoaded ++;
                    break;
            }
        }
        Assert.AreEqual(3,zonesLoaded,
            "Did not loaded all the zones saved");
    }
    
  3. Compile the program and run the test, using either the NUnit GUI app or the NUnitConsole. The tests should pass, indicating that we are now happy to move on. We can now test each area of functionality more fully without being concerned that we are wasting our time because the whole library does not actually work.

    This BreadthTest function is fairly large and takes a bit of time to run. If you have a few test methods like this, the time it takes to run all the tests will start to become annoying to most developers. The developers will stop running the tests on a regular basis. For this reason, it is worth putting tests such as this BreadthTest method in a suite (or fixture) of “long” tests that get run every night only as part of the nightly build process. Of course, a developer can always run these long tests manually if he suspects something might have changed that will cause these tests to fail.

Exercise 8-7: The Functional Depth Test

Now that we have proven that the overall functionality of the library meets our requirements, we can do some deeper testing of the individual units of functionality we want to use. For this example, we write tests only for the static LoadFromFile method of the TimeZonesFile class. This exercise should provide you with enough knowledge to enable you to write your own depth testing methods when you need them.

  1. In the same C# console project, create a new class called TestTimeZonesFile and set it up as before to be a test fixture using the attribute.

    Add a new Test method to this class called LoadFromFileTest.

    using System;
    using System.Globalization;
    using TimeZoneSerializer;
    using NUnit.Framework;
    
    namespace TimeZones
    {
        [TestFixture]
        public class TestTimeZonesFile
        {
            [Test]
            public void LoadFromFileTest()
            {
            }
         }
    }
    
  2. In the LoadFromFileTest method, we are going to check that the library can load files that exist, handles loading nonexistent files, and is consistent in its behavior. To write these tests, we need to have some files that we can load. We will write a method called HelperLoadFromFile. This method writes out three files: an empty file, a file containing one time zone, and a file containing three time zones. If you are following along with these exercises, you might want to rename the location of your test files. These test files, once created, should become part of your project and be checked into your source control software.

    [Test]
    public void HelperLoadFromFile()
    {
         TimeZonesFile zoneDoc = new TimeZonesFile();
         zoneDoc.WriteToFile(
               @"C:WorkTestFilesEmptyTimeZoneFile.xml");
         TimeZoneData zoneLondon = new TimeZoneData();
         zoneLondon.m_strPlace = "London";
         zoneLondon.m_nBias                 = 0;
         zoneLondon.m_nDaylightBias     = -60;
         zoneLondon.m_nStandardBias     = 0;
         zoneLondon.m_DaylightDate = DateTime.Parse(
                "03/31/2002 01:00",
                DateTimeFormatInfo.InvariantInfo);
         zoneLondon.m_StandardDate = DateTime.Parse(
                "10/27/2002 02:00",
                DateTimeFormatInfo.InvariantInfo);
         zoneDoc.AddZone(zoneLondon);
         zoneDoc.WriteToFile(
                @"C:WorkTestFilesSingleTimeZoneFile.xml");
    
         TimeZoneData zoneSydney = new TimeZoneData();
         zoneSydney.m_strPlace ="Sydney";
         zoneSydney.m_nBias      =-600;
         zoneSydney.m_nDaylightBias      =-60;
         zoneSydney.m_nStandardBias      =0;
         zoneSydney.m_DaylightDate = DateTime.Parse(
               "10/27/2002 02:00",
               DateTimeFormatInfo.InvariantInfo);
         zoneSydney.m_StandardDate = DateTime.Parse(
               "03/31/2002 03:00",
               DateTimeFormatInfo.InvariantInfo);
         zoneDoc.AddZone(zoneSydney);
    
         TimeZoneData zoneLA = new TimeZoneData();
         zoneLA.m_strPlace = "L.A.";
         zoneLA.m_nBias          =480;
         zoneLA.m_nDaylightBias  =-60;
         zoneLA.m_nStandardBias =0;
         zoneLA.m_DaylightDate = DateTime.Parse(
               "04/07/2002 02:00",
               DateTimeFormatInfo.InvariantInfo);
         zoneLA.m_StandardDate = DateTime.Parse(
               "10/27/2002 02:00",
               DateTimeFormatInfo.InvariantInfo);
         zoneDoc.AddZone(zoneLA);
    
         zoneDoc.WriteToFile(
             @"C:WorkTestFilesMultipleTimeZoneFile.xml");
    }
    
  3. Compile the program and run the TestTimeZonesFile test fixture. This will run two tests, the empty TestLoadFromFile method and the TestHelperLoadFromFile. After you have run the test, you can have a look at the files generated. Now comment out the Test attribute from the helper method; hopefully we won’t need that again.

    The files we have just created should be fixed and, as mentioned before, checked into your version control system. We can now use these files to test the behavior of the LoadFromFile method.

  4. In the TestLoadFromFile method, we can start by loading these files and checking they have the correct number of time zones. This might seem a little simplistic, and people often say, “Why write this test? It doesn’t prove anything; it’s too simple.” There is a lot of benefit to having simple tests. It is often the simple things that point out problems for us. Simple tests that prove the obvious are good, and once written we don’t need to revisit them. These tests will just run and run, making sure the obvious doesn’t ever get overlooked!

    [Test]
    public void LoadFromFileTest()
    {
        TimeZonesFile tzFile = TimeZonesFile.LoadFromFile
             (@"C:WorkTestFilesEmptyTimeZoneFile.xml");
        Assert.AreEqual(0,tzFile.Zones.Count,
            "Empty Time Zones File contains a time zone");
    
        tzFile = TimeZonesFile.LoadFromFile
             (@"C:WorkTestFilesSingleTimeZoneFile.xml");
        Assert.AreEqual(1,tzFile.Zones.Count,
        "Single Time Zones File contains incorrect number of time zones");
        tzFile = TimeZonesFile.LoadFromFile
             (@"C:WorkTestFilesMultipleTimeZoneFile.xml");
        Assert.AreEqual(3,tzFile.Zones.Count,
        "Multiple Time Zones File contains incorrect number of time zones");
    }
    
  5. Compile and run the tests in the TestTimeZoneFile fixture. There should be one test (remember, we commented out the other one), and it should pass.

  6. Next we can beef this test up by checking some of the zones we have loaded are as expected.

    [Test]
    public void LoadFromFileTest()
    {
        TimeZonesFile tzFile = TimeZonesFile.LoadFromFile
             (@"C:WorkTestFilesEmptyTimeZoneFile.xml");
        Assert.AreEqual(0,tzFile.Zones.Count,
             "Empty Time Zones File contains a time zone");
    
        tzFile = TimeZonesFile.LoadFromFile
             (@"C:WorkTestFilesSingleTimeZoneFile.xml");
        Assert.AreEqual(1,tzFile.Zones.Count,
          "Single Time Zones File contains incorrect number of time zones");
    
        TimeZoneData zoneLondon = new TimeZoneData();
        zoneLondon.m_strPlace = "London";
        zoneLondon.m_nBias              = 0;
        zoneLondon.m_nDaylightBias      = -60;
        zoneLondon.m_nStandardBias      = 0;
        zoneLondon.m_DaylightDate =
        DateTime.Parse("03/31/2002 01:00",
        DateTimeFormatInfo.InvariantInfo);
        zoneLondon.m_StandardDate =
        DateTime.Parse("10/27/2002 02:00",
        DateTimeFormatInfo.InvariantInfo);
    
        Assert.AreEqual(zoneLondon,tzFile.Zones[0],
            "Time zone loaded from single time zone file incorrect");
    
        tzFile = TimeZonesFile.LoadFromFile
            (@"C:WorkTestFilesMultipleTimeZoneFile.xml");
        Assert.AreEqual(3,tzFile.Zones.Count,
        "Mulitple Time Zones File contains incorrect number of time zones");
    
        Assert.AreEqual(zoneLondon,tzFile.Zones[0],
          "London time zone loaded from multiple time zone file incorrect");
        TimeZoneData zoneLA = new TimeZoneData();
        zoneLA.m_strPlace = "L.A.";
        zoneLA.m_nBias                 =480;
        zoneLA.m_nDaylightBias     =-60;
        zoneLA.m_nStandardBias     =0;
        zoneLA.m_DaylightDate = DateTime.Parse("04/07/2002 02:00",
            DateTimeFormatInfo.InvariantInfo);
        zoneLA.m_StandardDate = DateTime.Parse("10/27/2002 02:00",
            DateTimeFormatInfo.InvariantInfo);
    
        Assert.AreEqual(zoneLA ,tzFile.Zones[2],
          "LA time zone loaded from multiple time zone file incorrect");
    }
    
  7. Compile and run the tests again; they should pass.

At this point, we have tested that the TimeZonesFile.LoadFromFile method behaves as expected under normal conditions. We have also protected ourselves against future versions of the library not supporting the same file format.

If a new version of the library becomes available, we can run the tests against it. If the tests fail to load these files correctly, it would indicate an issue with backward compatibility. We could make a decision then as to whether we adopt the new version of the library or stay with the existing older version. A new version is likely to have some extra features that are attractive. If many of the tests against that library fail, it could be a strong indicator that the cost of going to the new version of the library means that it is not a viable option.

The next exercise explores how we can handle the exceptional cases and test for behavior outside the boundaries of the method’s capabilities.

Exercise 8-8: Testing for Exceptions

We have tested the breadth of the functionality of the library, and we have tested some expected behavior of one particular function (LoadFromFile) in the library. It would be worth testing how this function behaves when given spurious input or in exceptional cases.

  1. The first obvious case to test for is when the file doesn’t exist. We would expect that the function would throw a standard exception if the file doesn’t exist, something like a System.IO.FileNotFound. NUnit supports a C# attribute ExpectedException, which takes the type of exception expected. If the code in the method causes the exception to be thrown, the test passes; otherwise, it fails.

    [ExpectedException(typeof(System.IO.FileNotFoundException))]
    public void TestLoadFromFileNonExistant()
    {
         TimeZonesFile tzFile = TimeZonesFile.LoadFromFile
                     (@"C:WorkTestFilesNo such file.xml");
    }
    
  2. Compile and run the tests; there should be two tests now, and they should both pass.

  3. As an exercise for you, think of other cases where the library might fail and write some test methods to confirm your thoughts. (I can think of at least three things to test for.)

After you have finished writing the breadth and depth tests for a library, you can confidently use it with the knowledge that you have protected yourself against any unexpected behavior in this library or any future versions of it. The other thing you have achieved is to have documented the features of the library that you are using. This documentation in the form of executing code is the most valuable documentation I have ever found, because if the tests run it must be right.

In the following exercise, we work with a library that was written by a former employee late one night just before he left the company. Needless to say, it has been causing a few problems, and we have been asked to get it working. The library is reasonably simple. It contains one function for calculating some derived data for a stock. You can download the source code for the library from http://www.eXtreme.NET.Roodyn.com/BookExercises.aspx.

Exercise 8-9: Examining the Code

Most of the code for this project is in one file, StockData.cs. Examine this code and see how many observations you can make in one minute. Don’t try to walk through the functionality; just see what you can spot at first glance.

using System;
using System.Globalization;
using System.Xml;
using System.IO;

namespace StockDataLib
{
    public class StockData
    {
        public StockData(){}

        public void CalcDataForDay(string stock, DateTime day,
            ref float open, ref float cloase,ref float high,
            ref float low, ref float average)
        {
            //open the file for the stock
            FileStream file =
                new FileStream(@"c:stocks" + stock + ".stk",
                FileMode.Open);

            //read in all the prices
            XmlDocument doc = new XmlDocument();
            doc.Load(file);

            //get the prices for the day
            float[] prices = new float[1024];
            int n = 0;
            foreach (XmlNode price in
                doc.DocumentElement.ChildNodes)
            {
                if (day.Date ==
                    DateTime.Parse(price["DateTime"].InnerText,
                    DateTimeFormatInfo.InvariantInfo).Date)
                {
                    prices[n] =
                     (float)Convert.ToDouble(price["Price"].InnerText);
                     n++;
                }
            }
            // calc the data
            float tmpHigh = 0;
            for (int i = 1; i<prices.Length; i++)
            {
               if (prices[i] > tmpHigh)
               {
                   tmpHigh = prices[i];
               }
            }
   
            float tmpLow = 0;
            for (int i = 1; i<prices.Length; i++)
            {
               if (prices[i] < tmpLow)
               {
                   tmpLow = prices[i];
               }
            }

            float tmpAverage = 0;
            for (int i = 1; i<prices.Length; i++)
            {
                tmpAverage += prices[i];
            }
            tmpAverage = tmpAverage/(n-1);

            high = tmpHigh;
            low = tmpLow;
            average = tmpAverage;
        }
    }
}
//struct PriceTime
//{
//   public float price;
//   public DateTime time;
//}

Here are a number of things to note about this code:

  1. There is no test code.

  2. The function is reasonably long.

  3. The function does more than one thing.

  4. Two of the parameters. (Open and Close aren’t used.)

  5. The Close parameter is misspelled (indicates a lack of care).

  6. There is a PriceTime structure that is commented out. (So why is it there?)

  7. It uses hard-coded values (magic numbers), the path for the file, and the size of the prices array.

It would be tempting to get started and change some of the obvious problems (such as the hard-coded values) before dealing with the functionality. I always believe we should code test first and get a solution that meets our customer’s needs as quickly as possible and to the highest quality. This means even changing the spelling of one parameter should not be done without having a test; the change in the spelling is a refactor and should not be done without a test.

So the first thing we’re going to do is write some test code for this function.

Exercise 8-10: Writing a Breadth Test

We need to add a fixture and some test functionality to our library so we can validate its behavior.

  1. As in the previous exercises, add a reference to the NUnit.Framework.dll.

  2. Add a new StockDataTests class to the project and add the TestFixture attribute to the class.

  3. If you now compile the project and run it through the NUnit GUI, you will see that the fixture called StockDataTests turns yellow. This is to indicate that you have an empty fixture, which is something unusual and therefore highlighted.

  4. We will create a breadth test first, so create a new test method in the StockDataTests class called BreadthTestCalcDataForDay.

  5. Next we are going to write the test to use some data from a made-up test stock; edit your StockDataTests class file so it reads as shown.

    using System;
    using System.Globalization;
    using NUnit.Framework;
    
    namespace StockDataLib
    {
        [TestFixture] public class StockDataTests
        {
            [Test]public void BreadthTestCalcDataForDay()
            {
                StockData sData = new StockData();
    
                float open = 0.0F;
                float close = 0.0F;
                float high = 0.0F;
                float low = 0.0F;
                float average = 0.0F;
                string stock = "TestStock1";
                DateTime day = DateTime.Parse("03/27/2002 08:58",
                    DateTimeFormatInfo.InvariantInfo);
    
                sData.CalcDataForDay(stock, day,
                    ref open, ref close, ref high, ref low, ref average);
    
                Assertion.AssertEquals(
                     "Invalid Open in TestStock1",1.5000,open);
                Assertion.AssertEquals(
                     "Invalid Close in TestStock1",1.7000,close);
                Assertion.AssertEquals(
                     "Invalid High in TestStock1",1.8000,high);
                Assertion.AssertEquals(
                     "Invalid Low in TestStock1",1.4000,low);
                Assertion.AssertEquals(
                     "Invalid Average in TestStock1",1.6000,average);
            }
        }
    }
    
  6. Compile this and run the test through the NUnit GUI. You should see that it fails because it cannot find the file c:stocksTestStock1.stk. So we are going to need to create a file for our test stock. This file must contain data that would generate the results we want in the test we have just written.

    But how do we know what the format for the stock price file is?

    The first port of call should be your customer; this is one of the reasons that an XP practice is the onsite customer. If we couldn’t ask the customer a question, we would have to do our best by working out what the file format should be by working through the code or reading the documentation (which doesn’t exist!).

    The customer for this class library is another development team that needs the library. Asking the customer, we discover that the format is XML (as you probably guessed from looking at the code). This development team provides us with the following example.

    <?xml version="1.0" encoding="utf-8" ?>
    <Stock>
        <PriceTime>
            <DateTime>30/01/2002 09:30</DateTime>
            <Price>9.9999</Price>
        </PriceTime>
    </Stock>
    

    With a PriceTime for each price at a time and saved as the stockname with an .stk extension, this format is not exactly efficient, but we will go with it for the moment. We will create a file that we would expect to give us the results we want from the test.

    <?xml version="1.0" encoding="utf-8" ?>
    <Stock>
        <PriceTime>
            <DateTime>03/26/2002 09:30</DateTime>
            <Price>3.5000</Price>
        </PriceTime>
        <PriceTime>
            <DateTime>03/27/2002 09:00</DateTime>
            <Price>1.5000</Price>
        </PriceTime>
        <PriceTime>
            <DateTime>03/27/2002 13:30</DateTime>
            <Price>1.8000</Price>
        </PriceTime>
        <PriceTime>
            <DateTime>03/27/2002 14:45</DateTime>
            <Price>1.4000</Price>
        </PriceTime>
        <PriceTime>
            <DateTime>03/27/2002 16:30</DateTime>
            <Price>1.7000</Price>
        </PriceTime>
        <PriceTime>
            <DateTime>03/28/2002 14:48</DateTime>
            <Price>2.5000</Price>
        </PriceTime>
        <PriceTime>
            <DateTime>03/28/2002 16:30</DateTime>
            <Price>3.0000</Price>
        </PriceTime>
    </Stock>
    

    Don’t forget to save the file in a directory called stocks on your C: drive (we’ll come back to this later) and name it TestStock1.stk.

  7. Now rerun the test through the NUnit GUI. You should now get a different error: Invalid Open in TestStock1. We can now get about the business of fixing the code.

Exercise 8-11: Getting the Breadth Test Running

In this exercise, we walk through getting part of the test we just wrote to work. Then I leave it to you as an exercise to get the rest of this test running.

We start by fixing the problem reported with the open price. If you look through the code, you will see that the open parameter is not even used, so it is not surprising that it is not correct!

  1. The open is the first price of the day. If we look through the code, we can see that there is a loop that walks through all the prices in the file and puts the prices for the day into a prices array. So we need to copy the first value of that array to the open parameter.

    open = prices[0];
    
  2. Compile and run the tests. You should now see that it passes the assert for the open price. The assert for the close price is now failing. You can now fix the rest of this test yourself; the close is the last price of the day, the high is the highest price that day, the low is lowest price of the day, and the average is the sum of all the day’s prices divided by the number of prices that day.

    Remember to always do the simplest thing that will work and then move on. Work fast and furious.

    Your code should look something like this:

    public void CalcDataForDay(string stock, DateTime day,
        ref float open, ref float close,ref float high,
        ref float low, ref float average)
    {
        //open the file for the stock
        FileStream file =
            new FileStream(@"c:stocks" + stock + ".stk",
            FileMode.Open);
    
        //read in all the prices
        XmlDocument doc = new XmlDocument();
        doc.Load(file);
    
        //get the prices for the day
        float[] prices = new float[1024];
        int n = 0;
        foreach (XmlNode price in doc.DocumentElement.ChildNodes)
        {
            if (day.Date == DateTime.Parse(price["DateTime"].InnerText,
                DateTimeFormatInfo.InvariantInfo).Date)
            {
                prices[n] =
                    (float)Convert.ToDouble(price["Price"].InnerText);
                n++;
            }
        }
        open = prices[0];
        close = prices[n-1];
    
        // calc the data
        float tmpHigh = 0;
        for (int i = 1; i<prices.Length; i++)
        {
            if (prices[i] > tmpHigh)
            {
                tmpHigh = prices[i];
            }
        }
    
        float tmpLow = float.MaxValue;
        for (int i = 0; i<n; i++)
        {
            if (prices[i] < tmpLow)
            {
                tmpLow = prices[i];
            }
        }
    
        float tmpAverage = 0;
        for (int i = 0; i<n; i++)
        {
            tmpAverage += prices[i];
        }
        tmpAverage = tmpAverage/n;
    
        high = tmpHigh;
        low = tmpLow;
        average = tmpAverage;
    }
    

If you did something similar, congratulations! If you did significantly more, you did more than was necessary to fix the test. Pardon me? I thought I heard you say that you can see other problems with the code. Well, yes, there are lots of other problems with this code, and we are going to write new tests to fix those issues.

But why? Why not just fix the stuff that is broken when you can clearly see it is wrong?

The main reason is that the tests will document the code. When we see something that can break the code, we should write a test to break the code and then fix it. So let’s do that now. I am emphasizing this point again because it is so important to understand.

Exercise 8-12: Writing a Test to Break the Code

One of the places that looks to be obviously wrong is the calculation of the high. We changed the other two loops for calculating the low and the average, but we did not need to change the loop for the high because it passed the breadth test. We need to think how we can break the code and turn that idea into test code.

  1. We’ll start by writing a test that will test the high value.

    [Test]
    public void HighTestCalcDataForDay()
    {
        StockData sData = new StockData();
        float open = 0.0F;
        float close = 0.0F;
        float high = 0.0F;
        float low = 0.0F;
        float average = 0.0F;
        string stock = "TestHighFirst";
        DateTime day = DateTime.Parse("03/27/2002 08:58",
            DateTimeFormatInfo.InvariantInfo);
    
        sData.CalcDataForDay(stock, day,
            ref open, ref close, ref high, ref low, ref average);
    
        Assert.AreEqual(1.8000,high,
            "Invalid High in TestHighFirst");
    }
    
  2. Notice that we now have duplicate code that can be refactored into a setup method. We won’t do that until we have finished and have this test working. We might change what we have written, and then the refactoring would have been a waste of time.

  3. We now must create a file for this new test stock. My suspicion is that the code will fail if the high is the first price of the day, so we create a file that reflects this and save it as TestHighFirst.stk in the C:stocks directory.

    <?xml version="1.0" encoding="utf-8" ?>
    <Stock>
            <PriceTime>
                    <DateTime>03/26/2002 09:30</DateTime>
                    <Price>3.5000</Price>
            </PriceTime>
            <PriceTime>
                    <DateTime>03/27/2002 09:00</DateTime>
                    <Price>1.8000</Price>
            </PriceTime>
            <PriceTime>
                    <DateTime>03/27/2002 13:30</DateTime>
                    <Price>1.7000</Price>
            </PriceTime>
            <PriceTime>
                    <DateTime>03/27/2002 14:45</DateTime>
                    <Price>1.6000</Price>
            </PriceTime>
            <PriceTime>
                    <DateTime>03/27/2002 16:30</DateTime>
                    <Price>1.5000</Price>
            </PriceTime>
            <PriceTime>
                    <DateTime>03/28/2002 14:48</DateTime>
                    <Price>2.5000</Price>
            </PriceTime>
            <PriceTime>
                    <DateTime>03/28/2002 16:30</DateTime>
                    <Price>3.0000</Price>
            </PriceTime>
    </Stock>
    
  4. Compile and run the test from NUnit, and we discover that sure enough the high is calculated incorrectly. So now we must fix the code.

  5. The fix is a simple matter of changing the loop control statement.

    public void CalcDataForDay(string stock, DateTime day,
          ref float open, ref float close,ref float high,
          ref float low, ref float average)
    {
        .
        .
        .
    
        // calc the data
        float tmpHigh = 0;
        for (int i = 0; i<n; i++)
        {
            if (prices[i] > tmpHigh)
            {
               tmpHigh = prices[i];
            }
        }
        .
        .
        .
    }
    
  6. Compile the code and run the tests. They should succeed.

    This is a good example of a very simple code change required to fix a bug. The advantage of writing a test is that you have encoded the correct behavior into code that will be run every time the tests are run (which should be often). It is a preventive measure that will protect the code from being broken in the same way again.

  7. Now we can refactor the test code to place the duplicate code in a set up method.

    Your test class code should read as follows.

    [TestFixture]
    public class StockDataTests
    {
        StockData sData;
        float open;
        float close;
        float high;
        float low;
        float average;
    
        [SetUp]public void Init()
        {
            sData = new StockData();
    
            open = 0.0F;
            close = 0.0F;
            high = 0.0F;
            low = 0.0F;
            average = 0.0F;
        }
    
        [Test]
        public void BreadthTestCalcDataForDay()
        {
            string stock = "TestStock1";
            DateTime day = DateTime.Parse("03/27/2002 08:58",
                DateTimeFormatInfo.InvariantInfo);
    
            sData.CalcDataForDay(stock, day,
                ref open, ref close, ref high, ref low, ref average);
    
            Assert.AreEqual(1.5000, open,
                "Invalid Open in TestStock1");
            Assert.AreEqual(1.7000,close,
                "Invalid Close in TestStock1");
            Assert.AreEqual(1.8000,high,
                "Invalid High in TestStock1");
            Assert.AreEqual(1.4000,low,
                "Invalid Low in TestStock1");
            Assert.AreEqual(1.6000,average,
                "Invalid Average in TestStock1");
        }
    
        [Test]
        public void HighTestCalcDataForDay()
        {
             string stock = "TestHighFirst";
             DateTime day = DateTime.Parse("03/27/2002 08:58",
                 DateTimeFormatInfo.InvariantInfo);
    
             sData.CalcDataForDay(stock, day,
                 ref open, ref close, ref high, ref low, ref average);
    
             Assert.AreEqual(1.8000,high,
                  "Invalid High in TestHighFirst");
         }
    }
    

We have written a breadth test to validate overall functionality and a test to fix a piece of code that we could see was obviously wrong. Now I want us to return to a problem that you might have noticed when running the tests. When you run the tests multiple times in a row without reloading the assembly, you can get an error: The process cannot access the file.... This is a classic example of a problem that you might encounter in the wild, in that it occurs intermittently. We need to write a test that will force it to happen every time.

Exercise 8-13: Forcing an Intermittent Error

  1. It appears that the error occurs when you run the code through its paces several times in a row, so we’ll start by writing a simple test that just calls the method multiple times.

    [Test]
    public void MultipleTestCalcDataForDay()
    {
        string stock = "TestStock1";
        DateTime day = DateTime.Parse("03/27/2002 08:58",
            DateTimeFormatInfo.InvariantInfo);
    
        sData.CalcDataForDay(stock, day,
            ref open, ref close, ref high, ref low, ref average);
        sData.CalcDataForDay(stock, day,
            ref open, ref close, ref high, ref low, ref average);
    
        sData.CalcDataForDay(stock, day,
            ref open, ref close, ref high, ref low, ref average);
    }
    
  2. Compile and run the tests a few times. You should see that while the other two tests sometimes fail, because of this error, this last test always fails. Now we have a test that always fails we can set about fixing the bug.

  3. Looking through the code, we can see that the stock file that is opened and the method is never closed, so we can add the following line to the end of the method.

    file.Close();
    
  4. Now compile again and run the tests. You can run the tests a few times and you’ll see that we have fixed the bug.

Again this is a simple fix, but we wrote test code to protect ourselves from changes in the future breaking the code again in the same way.

You have seen how to add tests to code that was written without testing in mind and then fix some bugs in that code. We did testing for overall functionality for specific issues that we can spot are incorrect and coping with inconsistent behavior.

Coding with Protection

In the next two exercises, I want you to use a third-party library and protect yourself against changes that might occur in future versions of the library.

Exercise 8-14: Protecting Yourself Against the Change

We are building an application for tourists. This application will provide tourists with information about locations they may want to visit. We are going to use a class library (called WhereIs.dll) that is being written by another team. The first version they give us supports two methods in a Search class:

  • FindCity, which returns the capital city if we give it a country name

  • FindAttraction, which returns a popular tourist attraction for a capital city

We know that the library is still in development, but we need to start our project now so that we can show something to the customer as soon as possible. You can download the library from http://eXtreme.NET.Roodyn.com/BookExercises.aspx.

The countries currently supported are Australia, Canada, Denmark, England, France, Japan, and the United States.

We need to create a set of tests to validate the behavior of this library and that will protect us against any changes in future versions breaking our code without us knowing about it. Then use the library to create a simple Windows Forms application to return capital cities and attractions when a user enters a country. I’ll help you get started.

  1. Create a new C# application called WhereIsClient.

  2. Add a class called WhereIsTests and add the TestFixture attribute to the class.

  3. Create a new Test method in the class called TestFindCapital.

    [Test]
    public void TestFindCapital()
    {
         Search whereis = new Search();
         string city = string.Empty;
         city = whereis.FindCapital("England");
         //fill in the rest here
    }
    

I’ll leave the rest to you.

Hint: You might need to run the functions to see what they output before completing the tests.

Exercise 8-15: Plugging In the New Changed Library

The team building the WhereIs.dll class library have just released a new version that supports a method to get the next two days of weather for a capital city. The method is called FindWeather and takes a city name. It returns a string containing XML for the next two days’ weather. The team tells us that the XML returned looks like this.

<Weather>
     <Today>
          Sunny
     </Today>
     <Tomorrow>
          Rain
     </ Tomorrow >
</Weather>

You can download the new version of the library from http://eXtreme.NET.Roodyn.com/BookExercises.aspx.

Use the tests we have previously written to check for any changes in the class library. Then write a test (or tests) to validate the behavior of the new method, and then use the method to show the weather in the city displayed.

Conclusion

With the emphasis firmly on the importance of testing in XP, this chapter has provided you with tools you need to tests user interfaces and other people’s code. The lesson I hope you take away from this chapter is that you can write tests to cover most eventualities, and you should always start the process of development by writing the tests first. The tests validate the quality of your code and document your assumptions as to how the code will be used. They provide the communication and the feedback while driving you toward simpler solutions. The tests embody these three values that XP holds in high regard.

This chapter explored two techniques you can use to write more testable user interfaces. First, you learned how by encapsulating more functionality in classes away from the user interface, you can develop tests first and build code that is user-interface agnostic. Then this chapter examined how you can use reflection to call methods that are attached to the user interface.

Then this chapter covered how to deal with using third-party libraries and validate that they are performing how we expect and provide the functionality we require. Finally, we walked through some exercises in fixing existing code that contains bugs.

All that’s left to do now is put all these practices together. In the next chapter, we do exactly that; we will work on a project using all the practices covered in this book.

1.

I have more than once made a recommendation to ditch some code that attempted to do far more than was required of it and was not working correctly. Gold plating is one of the evils of software development that eXtreme Programming attempts to remove from the equation.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
3.128.226.121