Introduction - Defining the Battlefield
This tutorial is an short introduction to using Test Driven Development (TDD) in Visual Studio 2010 (VS2010) with C#. Like most of my examples it's based on a game.
By completing this tutorial you will:
- Get a taste of TDD through a series of small iterations;
- Learn how VS2010 provides TDD support through a number of new features; and
- Learn a number of C# 4.0 features.
CannonAttack is a simple text based game in which a player enters an angle and velocity of a cannonball to hit a target at a given distance. The game uses a basic formula for calculating the trajectory of the cannonball and the player keeps taking turns at shooting at the target until it has been hit. I won't go into TDD theory in any great detail now, but you should check out the number of great references to TDD including:
http://en.wikipedia.org/wiki/Test_driven_development
http://www.codeproject.com/KB/dotnet/tdd_in_dotnet.aspx
C# .NET 2.0 Test Driven Development by Matthew Cochran
http://msdn.microsoft.com/en-us/library/dd998313(VS.100).aspx
The following are the fundamental steps of a TDD iteration:
- RED - take a piece of functionality and build a test for it and make it fail the test by writing a minimum amount of code(basically just get it to compile and run the test);
- GREEN - write minimal code for the test to make it succeed; and
- REFACTOR - clean up and reorganize the code and ensure it passes the test and any previous tests.
In this tutorial we will be progressing through a number of iterations of the TDD cycle to produce a fully functional simple application. Each iteration will pick up one or more of the requirements/specs from our list (see The CannonAttack Requirements/Specs below). We won't test for absolutely everything and some of the tests are fairly basic and simplistic, but I am just trying to keep things reasonably simple at this stage
VS2010 and C# 4.0:
This tutorial covers the use of VS2010 and targets a number of features of C# 4.0,
- Generating stubbs for TDD in VS2010;
- Test Impact View in VS2010;
- Tuples; and
- String.IsNullOrWhiteSpace method.
There are many more features of C#4.0 and we will be covering them in future tutorials.
What you need:
- This is a C# tutorial so a background in C# would be very useful; and
- VS 2010 professional or above (This tutorial has been tested against VS2010 rtm and beta2).
The CannonAttack Requirements/Specs:
The following is a combination of Requirements and Specifications that will give us some guide in terms of the application we are trying to build:
- Windows Console Application;
- Player identified by an id, default is set to a constant "Human";
- Single player only, no multi-play yet;
- Allow player to set Angle and Speed of the Cannon Ball to Shoot at a Target;
- Target Distance is simply the distance of the Cannon to Target, and is created randomly by default but can be overridden;
- Angle and Speed needs to be validated (specifically not greater than 90 degrees and Speed not greater than speed of light);
- Max distance for target is 20000 meters;
- Base the algorithm for the calculation of the cannons trajectory upon the following C# code (distance and height is meters and velocity is meters per second):
distance = velocity * Math.Cos(angleInRadians) * time;
height = (velocity * Math.Sin(angleInRadians) * time) - (GRAVITY * Math.Pow(time, 2)) / 2;
- A hit occurs if the cannon is within 50m of the target;
- Display number of shots for a hit
- Game text will be similar to following:
Welcome to Cannon Attack
Target Distance:12621m
Please Enter Angle:40
Please Enter Speed:350
Missed cannonball landed at 12333m
Please Enter Angle:45
Please Enter Speed:350
Hit - 2 Shots
Would you like to play again (Y/N)
Y
Target Distance:2078m
Please Enter Angle:45
Please Enter Speed:100
Missed cannonball landed at 1060m
Please Enter Angle:45
Please Enter Speed:170
Missed cannonball landed at 3005m
Please Enter Angle:45
Please Enter Speed:140
Hit - 3 shots
Would you like to play again (Y/N)
N
Thanks for playing CannonAttack
OK so now we are ready to code, let's go...
Iteration 1 - Creating the Cannon
Steps
- Start Visual Studio
- Click New Project...
- From the Windows submenu select Console Application as below
- Call the application CannonAttack and click OK
- Right click on Program.cs and select Rename and call the file CannonAttack.cs.
- If you see the following select YES.
- When the solution has loaded right click on the solution in Solution Explorer and select ADD->NEW PROJECT. If you can't see the Solution Explorer select from Tools->Options->Projects and Solutions and tick always show solution
- Select Test Project and call it CannonAttackTest as below:
- Click OK
- Right click on the CannonAttackTest project and make it the startup project by selecting Set as Startup Project.
- Rename the UnitTest1.cs to CannonAttackTest.cs
- If you see the following select YES
- You should see the Solution explorer appear as:
- Open the code window for the CannonAttackTest.cs file and you should see the default test method. The [TestMethod] attribute above a method in this file indicates VS2010 will add this method as a unit test.
- Replace the default test method:
[TestMethod]
public void TestMethod1()
{
}
with the following:
[TestMethod]
public void TestCannonIDValid()
{
Cannon cannon = new Cannon();
}
- This won't compile but right click on Cannon and select Generate->New Type
- When the following screen appears change the Project to CannonAttack and accept the other defaults as below:
- Now change the TestCannonIDValid method to:
[TestMethod]
public void TestCannonIDValid()
{
Cannon cannon = new Cannon();
Assert.IsNotNull(cannon.ID);
}
- Now the solution won't compile as the ID property does not exist
- Right click on the ID and select Generate->Property (ID has now been added to the Cannon class). This creates an auto property which for the moment will hopefully compile.
- Save all projects and hit CTRL-F5. This will start the Tests running (not F5 in debug mode because debug mode will by default stop if an Assert fails).
- You should see in the tests window:
- The test failed. This is correct and as expected the first phase of the TDD iteration has occurred RED - failed!!!
- So now that we have a red lets get this test to pass.
- Select cannon.cs in the CannonAttack project and make the following change to the ID property:
public string ID
{
get
{
return "Human";
}
}
- Now run the test CTRL-F5 and we should see:
- We have just completed the second stage of a TDD cycle GREEN - Pass!!!!
- So the next stage is to refractor, and it would be nice to make a couple of changes to the class to clean it up, so that the class now looks like:
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
namespace CannonAttack
{
public sealed class Cannon
{
private readonly string CANNONID = "Human";
private string CannonID;
public string ID
{
get
{
return (String.IsNullOrWhiteSpace(CannonID)) ? CANNONID : CannonID;
}
set
{
CannonID = value;
}
}
}
}
We have made this class sealed so that it is not inherited by anything. Also, we have added a readonly string to store a default ID if not set by the user. I am going to use runtime constants (readonly) because they are more flexible than compile time constants (const) and if you are interested check out Bill Wagner's book (effective C# - 50 Ways to Improve your C#) for further details.
Let's run the test again. Again it should compile and pass tests because although we have made some changes to the code, we should not have impacted the tests and this is an important part of the Refactor phase. We should make the changes we need to make the code more efficient and reusable, but it is critical that the same test that we made pass in the Green phase still passes in the Refactor phase.
The refactoring is complete. Now for ITERATION 2 of the CannonAttack project.
Iteration 2 - One Canon, and only one Cannon - Using the Singleton Pattern.
Like the previous iteration we will pick an element of functionality and work through the same sequence again RED->GREEN->REFACTOR. Our next requirement is to allow only a single player. Given that we can allow 1 player we really should only use one instance, let's create a test for only one instance of the cannon object. We can compare two objects to ensure they are pointing at the same instance like (obj == obj2).
- Add a new test beneath the first test in cannonattacktest.cs and our method looks like:
[TestMethod]
public void TestCannonMultipleInstances()
{
Cannon cannon = new Cannon();
Cannon cannon2 = new Cannon();
Assert.IsTrue(cannon == cannon2);
}
- Run the tests by hitting CTRL-F5. We 1 Pass and 1 fail (our new test) so we are at RED again.
- The reason that this failed is we have created two different instances. What we need is the singleton pattern to solve our problem. I have a great book on patterns called HEAD FIRST DESIGN PATTERNS if you want to know more about design patterns it's a great start - sure its Java but the code is so close to C# you should not have any real problems.
- We are going to use the singleton pattern to meet the requirement of a single player. Really we don't want multiple instances of cannons hanging around - 1 and only 1 instance is needed. Insert the following Singleton code below the property for the ID.
private static Cannon cannonSingletonInstance;
private Cannon()
{
}
public static Cannon GetInstance()
{
if (cannonSingletonInstance == null)
{
cannonSingletonInstance = new Cannon();
}
return cannonSingletonInstance;
}
- If we try to run the tests we won't compile because the cannon object can't be created with Cannon cannon = new Cannon(); So make sure that we use Cannon.GetInstance() instead of new Cannon(). The two test methods should now look like:
[TestMethod]
public void TestCannonIDValid()
{
Cannon cannon = Cannon.GetInstance();
Assert.IsNotNull(cannon.ID);
}
[TestMethod]
public void TestCannonMultipleInstances()
{
Cannon cannon = Cannon.GetInstance();
Cannon cannon2 = Cannon.GetInstance();
Assert.IsTrue(cannon == cannon2);
}
- Run the Tests again CTRL-F5
This time they pass GREEN so time to refactor. We are going to change our Singleton code because although the code works (and is pretty much 100% the same as the singleton code in the HEAD FIRST DESIGN PATTERNS Book) it is not thread safe in C# (see http://msdn.microsoft.com/en-us/library/ff650316.aspx) So we replace the original singleton code with:
private static Cannon cannonSingletonInstance;
static readonly object padlock = new object();
private Cannon()
{
}
public static Cannon GetInstance()
{
lock (padlock)
{
if (cannonSingletonInstance == null)
{
cannonSingletonInstance = new Cannon();
}
return cannonSingletonInstance;
}
}
The block inside the lock ensures that only one thread enters this block at any given time. Given the importance of determining if there is an instance or not, we should definitely use the lock.
- Run the all the tests again CTRL-F5 and they should all pass
That's the end of the second iteration and I think you should be getting the hang of it by now, so lets get onto the 3rd iteration.
Iteration 3 - Angling for something...
We will add another test method. This time we want to ensure that an incorrect angle (say 95 degrees) will not hit. So we need a Shoot method and a return type (lets keep it simple and make it a Boolean for now).
- Add the following test below the last test:
[TestMethod]
public void TestCannonShootIncorrectAngle()
{
Cannon cannon = Cannon.GetInstance();
Assert.IsFalse(cannon.Shoot(95, 100));
}
- Of course the compiler will complain and we get it to compile by right clicking on the Shoot method and select Generate->Method Stub.
- Change the return type of the stub (in Cannon.cs) to a bool so that it will compile.
public bool Shoot(int p, int p_2)
{
throw new NotImplementedException();
}
- Now hit CTRL-F5 and we see that the first two tests should still pass but our new test fails as this is the RED phase again.
- So open Cannon.cs in the CannonAttack project, and replace the generated Shoot method with:
public bool Shoot(int angle, int velocity)
{
if (angle > 90 || angle < 0) //Angle must be between 0 //and 90 degrees
{
return false;
}
return true; //Not going to do the calculation just yet
}
- Run the tests again and all three tests now pass so again :GREEN, so now back to refactor.
- We need to add two extra readonly integers to the class. Insert them at the top of the class. We will add them as public so that they are exposed to the console application eventually.
public static readonly int MAXANGLE = 90;
public static readonly int MINANGLE = 1;
- We want to refactor the Shoot method, so this now looks like:
public Tuple<bool, string> Shoot(int angle, int velocity)
{
if (angle > MAXANGLE || angle < MINANGLE) //Angle must be //between 0 and 90 degrees
{
return Tuple.Create(false, "Angle Incorrect");
}
return Tuple.Create(true, "Angle OK"); //Not //going to do the calculation just yet
}
We have changed the interface of the method so it returns a Tuple indicating if its a hit (BOOL) and also a message (STRING) containing the display text. The Tuple is a feature of C# 4.0. used to group a number of types together and we have used it in conjunction with the type inference of the var to give a neat and quick way to handle the messages from our shoot method. See the following article for further information:http://www.davidhayden.me/2009/12/tuple-in-c-4-c-4-examples.html
- To handle the change to the shoot method we change our test to:
[TestMethod]
public void TestCannonShootAngleIncorrect()
{
var shot = cannon.Shoot(95, 100);
Assert.IsFalse(shot.Item1);
}
You will also notice now we have removed the object initialization of the cannon in our test method. The reason we have done this is we have moved that to the Class Initialize method of CannonAttackTest.cs. To do this simply uncomment out the two lines below:
[ClassInitialize()]
public static void CannonTestsInitialize(TestContext testContext)
{
}
Rename MyClassInitialize to CannonTestsInitialize.
- Now just declare a cannon object for the class and add the object initialize code as this method is called every time we run our test. It means every test can remove that one line of code and now looks like the following (note we have removed a number of commented test methods that visual studio creates by default):
using System;
using System.Text;
using System.Collections.Generic;
using System.Linq;
using Microsoft.VisualStudio.TestTools.UnitTesting;
using CannonAttack;
namespace CannonAttackTest
{
[TestClass]
public class CannonAttackTest
{
private static Cannon cannon;
[ClassInitialize()]
public static void CannonTestsInitialize(TestContext testContext)
{
cannon = Cannon.GetInstance();
}
[TestMethod]
public void TestCannonIDValid()
{
Assert.IsNotNull(cannon.ID);
}
[TestMethod]
public void TestCannonMultipleInstances()
{
Cannon cannon2 = Cannon.GetInstance();
Assert.IsTrue(cannon == cannon2);
}
[TestMethod]
public void TestCannonShootAngleIncorrect()
{
var shot = cannon.Shoot(95, 100);
Assert.IsFalse(shot.Item1);
}
}
}
- Now let's compile and run the tests CTRL-F5 and we'll find that they should all pass so we are finished the 3rd iteration.
Iteration 4 - That's a pretty fast cannonball
The next area of functionality, looking at the requirements, is to validate the incorrect speed. Just incase you are worrying about this validation...
"Nothing in the universe can travel at the speed of light, they say, forgetful of the shadow's speed." ~Howard Nemerov
Now that we are informed, let's write the test...
- Add the following below the TestCannonShootAngleIncorrect method
[TestMethod]
public void TestCannonShootVelocityGreaterThanSpeedOfLight()
{
var shot = cannon.Shoot(45, 300000001);
Assert.IsFalse(shot.Item1);
}
- So we run the tests and it will fail RED because the validation isn't complete in the Shoot method . We add the code needed into shoot method just under the validation code for the incorrect angle:
if (velocity > 300000000)
{
return Tuple.Create(false, "Velocity of the cannon cannot travel faster than the speed of light");
}
- Run the tests again and this time they all pass - GREEN
- Time to refactor, but this time lets use a new feature in VS2010 that allows us to determine the impact of a code change on the Test project.
- Select from the menu TEST->Windows->Test Impact View :
- The Test Impact View will appear on the right of your IDE and if you haven't used Test Impact View yet you will see:
- Click the Enable test impact data collection in the active test settings link and then select OK on the next dialog. You must do this for each solution you use test impact view.
- Build and Run CTRL-F5 all the tests and again they should all pass. Time to refactor.
- Now add a constant (readonly int) for the speed of light and then ensure the code in the shoot method refers to the constant ie:
private readonly int MAXVELOCITY = 300000000;
And
if (velocity > MAXVELOCITY)
{
return Tuple.Create(false, "Velocity of the cannon cannot travel faster than the speed of light");
}
- Now build the project and have a look at the Test Impact View:
This is very useful as we can see the impact of that change (even though its not a serious change) on the tests. If you want to run just the tests impacted by the change click on the Run Tests link in the Test Impact View window. While I won't talk further about the test impact view, it's worth using in your TDD activities. OK so this iteration is complete now for the calculation of the cannonball trajectory.
Iteration 5 - Shoot, the Messenger:Shot!!!
Time for the real fun of the shoot method. As the next four requirements are very close, I am going to include them in this iteration. Normally we would break these up but to keep this tutorial as brief as possible we'll group them together. They are:
- Target Distance is simply the distance of the Cannon to Target, and is created randomly by default but can be overridden;
- Max distance for target is 20000 meters;
- Base the algorithm for the calculation of the cannons trajectory upon the following C# code (distance and height is meters and velocity is meters per second):
- distance = velocity * Math.Cos(angleInRadians) * time;
- height = (velocity * Math.Sin(angleInRadians) * time) - (GRAVITY * Math.Pow(time, 2)) / 2;
- A hit occurs if the cannon is within 50m of the target;
First, we need a test to set a distance for the target and the correct angle and velocity for a miss (yes we are going to test for a miss first). Actually, for this functionality we are going to add two tests: one to test a miss and one for a hit.
- So lets add the tests:
[TestMethod]
public void TestCannonShootMiss()
{
cannon.SetTarget(4000);
var shot = cannon.Shoot(45, 350);
Assert.IsTrue(shot.Item2 == "Missed cannonball landed at 12621 meters");
}
[TestMethod]
public void TestCannonShootHit()
{
cannon.SetTarget(12621);
var shot = cannon.Shoot(45, 350);
Assert.IsTrue(shot.Item2 == "Hit");
}
- This will not compile so we can right click SetTarget and select Generate Method Stub.
- Run the tests and of course the last two tests fail
- We need to some changes to make it succeed, add this private variable and readonly int to the class:
private int distanceOfTarget;
private readonly double GRAVITY = 9.8;
- below the validation code replace
return Tuple.Create(true, "Angle OK"); //Not going to do //the calculation just yet
with:
string message;
bool hit;
int distanceOfShot = CalculateDistanceOfCannonShot(angle, velocity);
if (distanceOfShot == distanceOfTarget)
{
message = "Hit";
hit = true;
}
else
{
message = String.Format("Missed cannonball landed at {0} meters", distanceOfShot);
hit = false;
}
return Tuple.Create(hit, message);
- and then we create another method
public int CalculateDistanceOfCannonShot(int angle, int velocity)
{
int time = 0;
double height = 0;
double distance = 0;
double angleInRadians = (3.1415926536 / 180) * angle;
while (height >= 0)
{
time++;
distance = velocity * Math.Cos(angleInRadians) * time;
height = (velocity * Math.Sin(angleInRadians) * time) - (GRAVITY * Math.Pow(time, 2)) / 2;
}
return (int)distance;
}
- Now Compile and run the tests and they will fail - because we are throwing an exception in SetTarget so lets change SetTarget to :
public void SetTarget(int distanceOfTarget)
{
this.distanceOfTarget = distanceOfTarget;
}
- Run the tests again and now they all pass GREEN. Time to refactor.
- Lets add some code to the constructor of the cannon class to randomly set it
private Cannon()
{
//by default we setup a random target
Random r = new Random();
SetTarget(r.Next(MAXDISTANCEOFTARGET));
}
- and a constant for MAXDISTANCEOFTARGET
private readonly int MAXDISTANCEOFTARGET = 20000;
- Make sure DistanceOfTarget is now a property:
public int DistanceOfTarget
{
get { return distanceOfTarget; }
set { distanceOfTarget = value; }
}
- And now SetTarget can change to:
public void SetTarget(int distanceOfTarget)
{
if (!distanceOfTarget.Between(0, MAXDISTANCEOFTARGET))
{
throw new ApplicationException(String.Format("Target distance must be between 1 and {0} meters", MAXDISTANCEOFTARGET));
}
this.distanceOfTarget = distanceOfTarget;
}
- And we need to include the burst radius code in the Shoot method which will replace:
if (distanceOfShot == distanceOfTarget)
- With
if (distanceOfShot.WithinRange(this.distanceOfTarget, BURSTRADIUS))
- and a couple of important changes here
- We are using an extension method to provide a number of additional functions to ints.
- To add an extension method we follow the following steps:
- Add a class to the console project
- Call it ExtensionMethods.cs
- Enter the following code:
using System;
namespace System
{
public static class ExtensionMethods
{
public static bool Between(this int source, int min, int max)
{
return (source >= min && source <= max);
}
public static bool WithinRange(this int source, int target, int offset)
{
return (target.Between(source - offset, source + offset));
}
}
}
- We also need a BURSTRADIUS constant added to the class:
private readonly int BURSTRADIUS = 50;
The reason we use an extension method is because the Between and Within Range will be used often within the app. As we used the System namespace for these extension methods they will be provided always.
OK, So run the tests again and we find that they pass so time to move onto the next iteration
Iteration 6 - Counting Shots not Crows.
So the last piece of functionality to test is the Number of Shots so lets start with a simple test.
- insert this after the last test.
[TestMethod]
public void TestCannonCountShots()
{
cannon.SetTarget(12621);
var shot = cannon.Shoot(45, 350);
Assert.IsTrue(shot.Item2 == "Hit - 1 Shot(s)", "Number of shots:" + cannon.Shots);
}
This will not compile because Shots doesn't exist yet.
- Right click on the Shots property and select Generate->Property
- Run the tests again and you will see this fail so lets make some changes to Cannon including:
- Run the tests again we see the following:
This tells us the shot count is 3 not 1 (it also shows that the original test for a hit has to change to handle the new text) and the reason for this is the singleton pattern we are using. As we are only maintaining one instance of the object, that instance is actually being used for every Shoot that occurs during the run of tests. So we need to ensure every time we start a new game (ie. a new test) we reset the shot count. The easiest way is to introduce a Reset method where we can reset the shot count but also later on we can use this method for other things that need to be cleaned up/reset.
- Add the following to Cannon.cs:
public void Reset()
{
shots = 0;
}
- The way we can use this in our tests is by using a method that is being called before every test. Add the following method to CannonAttackTest
[TestInitialize]
public void ResetCannonObject()
{
cannon.Reset();
}
- Let's change the original for test for the hit to:
[TestMethod]
public void TestCannonShootHit()
{
cannon.SetTarget(12621);
var shot = cannon.Shoot(45, 350);
Assert.IsTrue(shot.Item2 == "Hit - 1 Shot(s)");
}
- Run the tests CTRL-F5. They will all pass:
and the cannon class is pretty much done. This is the end of the 6th Iteration and the Classes are complete. We are now going to write the code for CannonAttack that will call the classes from the UI.
Completing the Project
We can writethe Code in the Console UI to get the values from the user for the angle. This is important part because we haven't thought about UI till now, All the UI should be handled by the console. To do this:
- Set the CannonAttack console project as the startup project
- Add the following code to the CannonAttack.cs file
using System;
namespace CannonAttack
{
class CannonAttack
{
private static readonly int MaxNumberOfShots = 50;
static void Main(string[] args)
{
Console.WriteLine("Welcome to CannonAttack");
bool isStillPlaying = true;
while (isStillPlaying)
{
bool isAHit = false;
Cannon cannon = Cannon.GetInstance();
while (!isAHit && cannon.Shots < MaxNumberOfShots)
{
int angle;
int velocity;
Console.WriteLine(String.Format("Target is at {0} meters", cannon.DistanceOfTarget));
GetInputVariable(out angle, out velocity);
var shot = cannon.Shoot(angle, velocity);
isAHit = shot.Item1;
Console.WriteLine(shot.Item2);
}
isStillPlaying = GetIsPlayingAgain();
cannon.Reset();
}
Console.WriteLine("Thanks for playing Cannon Attack");
}
}
}
This is the core of the application and it should provide the main basis for the cannon attack application.
Below are the additional helper methods that are needed, so add them in underneath the main method:
private static bool GetIsPlayingAgain()
{
bool isPlayingAgain = false;
bool validAnswer = false;
while (!validAnswer)
{
Console.Write("Do you want to play again (y/n)?");
string playAgain = Console.ReadLine();
if (playAgain == "y" || playAgain == "Y")
{
isPlayingAgain = true;
validAnswer = true;
}
if (playAgain == "n" || playAgain == "N")
{
isPlayingAgain = false;
validAnswer = true;
}
}
return isPlayingAgain;
}
private static void GetInputVariable(out int angle, out int velocity)
{
string angleBuffer;
string velocityBuffer;
Console.Write(String.Format("Enter Angle ({0}-{1}):", Cannon.MINANGLE, Cannon.MAXANGLE));
angleBuffer = Console.ReadLine();
if (!int.TryParse(angleBuffer, out angle))
{
Console.WriteLine("Not a number, defaulting to 45");
angle = 45;
}
Console.Write("Enter Velocity (meters per second):");
velocityBuffer = Console.ReadLine();
if (!int.TryParse(velocityBuffer, out velocity))
{
Console.WriteLine("Not a number, defaulting to 100 meters per second");
velocity = 100;
}
}
Now you should be able to run the application and it should work displaying the output similar to what we defined on page 2 and should look like:
This is the End
That's it - your first TDD project is complete!!!. The next tutorial will introduce a number of new areas of functionality including:
- Multiplayer; and
- Mountains.
Any feedback you have would be appreciated as I'll update this tutorial as needed.
Additional Resources