eXtreme .NET: Introducing eXtreme Programming Techniques to .NET Developers [Electronic resources] نسخه متنی

اینجــــا یک کتابخانه دیجیتالی است

با بیش از 100000 منبع الکترونیکی رایگان به زبان فارسی ، عربی و انگلیسی

eXtreme .NET: Introducing eXtreme Programming Techniques to .NET Developers [Electronic resources] - نسخه متنی

Neil Roodyn

| نمايش فراداده ، افزودن یک نقد و بررسی
افزودن به کتابخانه شخصی
ارسال به دوستان
جستجو در متن کتاب
بیشتر
تنظیمات قلم

فونت

اندازه قلم

+ - پیش فرض

حالت نمایش

روز نیمروز شب
جستجو در لغت نامه
بیشتر
لیست موضوعات
توضیحات
افزودن یادداشت جدید














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



In the next exercise, we learn how 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:\Work\TestFiles\EmptyTimeZoneFile.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:\Work\TestFiles\SingleTimeZoneFile.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:\Work\TestFiles\MultipleTimeZoneFile.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:\Work\TestFiles\EmptyTimeZoneFile.xml");
Assert.AreEqual(0,tzFile.Zones.Count,
"Empty Time Zones File contains a time zone");
tzFile = TimeZonesFile.LoadFromFile
(@"C:\Work\TestFiles\SingleTimeZoneFile.xml");
Assert.AreEqual(1,tzFile.Zones.Count,
"Single Time Zones File contains incorrect number of time zones");
tzFile = TimeZonesFile.LoadFromFile
(@"C:\Work\TestFiles\MultipleTimeZoneFile.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:\Work\TestFiles\EmptyTimeZoneFile.xml");
Assert.AreEqual(0,tzFile.Zones.Count,
"Empty Time Zones File contains a time zone");
tzFile = TimeZonesFile.LoadFromFile
(@"C:\Work\TestFiles\SingleTimeZoneFile.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:\Work\TestFiles\MultipleTimeZoneFile.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:\Work\TestFiles\No 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.


[1] I do not recommend throwing it away and starting again. Instead, leave what works alone. As the saying goes, "If it ain't broke, don't fix it." If, on the other hand, you have an area of code you need to enhance or somewhere there is a bug that needs to be fixed, I recommend fixing it by using the test-first approach.


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:



    There is no test code.



    The function is reasonably long.



    The function does more than one thing.



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



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



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



    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:\stocks\TestStock1.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


    / 117