![]() |
Platforms, Frameworks & Libraries »
.NET Framework »
General
Intermediate
License: The Code Project Open License (CPOL)
Test-Driven Development in .NETBy Peter ProvostAn article presenting benefits and techniques for using test-driven development in .NET. |
C#, Windows, .NET 1.0, Visual Studio, Dev
|
|
Advanced Search |
|
|
|
||||||||||||||||
TestFixture attribute is used to indicate that a class contains test methods. When you attach this attribute to a class in your project, the Test Runner application will scan it for test methods. The following code illustrates the usage of this attribute. (All of the code in this article is in C#, but NUnit will work with any .NET language, including VB.NET. See the NUnit documentation for additional information.)
namespace UnitTestingExamples
{
using System;
using NUnit.Framework;
[TestFixture]
public class SomeTests
{
}
}
The only restrictions on classes that use the TestFixture attribute are that they must have a public default constructor (or no constructor which is the same thing).
Test attribute is used to indicate that a method within a test fixture should be run by the Test Runner application. The method must be public, return void, and take no parameters or it will not be shown in the Test Runner GUI and will not be run when the Test Fixture is run. The following code illustrates the use of this attribute:
namespace UnitTestingExamples
{
using System;
using NUnit.Framework;
[TestFixture]
public class SomeTests
{
[Test]
public void TestOne()
{
// Do something...
}
}
}
SetUp) or after (Teardown) every test method in the Test Fixture. The most common use for these attributes is when you need to create dependent objects (e.g., database connections, etc.). This example shows the usage of these attributes:
namespace UnitTestingExamples
{
using System;
using NUnit.Framework;
[TestFixture]
public class SomeTests
{
private int _someValue;
[SetUp]
public void Setup()
{
_someValue = 5;
}
[TearDown]
public void TearDown()
{
_someValue = 0;
}
[Test]
public void TestOne()
{
// Do something...
}
}
}
ExpectedException attribute, as shown in the following example:
namespace UnitTestingExamples
{
using System;
using NUnit.Framework;
[TestFixture]
public class SomeTests
{
[Test]
[ExpectedException(typeof(InvalidOperationException))]
public void TestOne()
{
// Do something that throws an InvalidOperationException
}
}
}
When this code runs, the test will pass only if an exception of type InvalidOperationException is thrown. You can stack these attributes up if you need to expect more than one kinds of exception, but you probably should it when possible. A test should test only one thing. Also, be aware that this attribute is not aware of inheritance. In other words, if in the example above the code had thrown an exception that derived from InvalidOperationException, the test would have failed. You must be very explicit when you use this attribute.
Ignore attribute as follows:
namespace UnitTestingExamples
{
using System;
using NUnit.Framework;
[TestFixture]
public class SomeTests
{
[Test]
[Ignore("We're skipping this one for now.")]
public void TestOne()
{
// Do something...
}
}
}
If you feel the need to temporarily comment out a test, use this instead. It lets you keep the test in your arsenal and it will continually remind you in the test runner output.
Assertion class provides a variety of static methods you can use in your test methods to actually test that what has happened is what you wanted to happen. The following sample shows what I mean:
namespace UnitTestingExamples
{
using System;
using NUnit.Framework;
[TestFixture]
public class SomeTests
{
[Test]
public void TestOne()
{
int i = 4;
Assertion.AssertEquals( 4, i );
}
}
}
(I know that isn't the most relevant bit of code, but it shows what I mean.)
Run button. If you want to run only one Test Fixture or even just a single test, you can double-click it in the tree. The following screenshot shows the GUI app:
There are situations, particularly when you want to have an automated build script run your tests, when the GUI app isn't appropriate. In these automated builds, you typically want to have the output posted to a website or another place where it can be publicly reviewed by the development team, management or the customer. The NUnit 2.0 console application takes the assembly as a command-line argument and produces XML output. You can then use XSLT or CSS to convert the XML into HTML or any other format. For more information about the console application, check out the NUnit documentation.
BankAccount class, I create a class in my testing library called BankAccountTests. The first thing I need my bank account class to do is be able to take a deposit and show the correct balance. So I write the following code:
namespace UnitTestingExamples.Tests
{
using System;
using NUnit.Framework;
[TestFixture]
public class BankAccountTests
{
[Test]
public void TestDeposit()
{
BankAccount account = new BankAccount();
account.Deposit( 125.0 );
account.Deposit( 25.0 );
Assertion.AssertEquals( 150.0, account.Balance );
}
}
}
Once this is written, I compile my code. It fails of course because the BankAccount class doesn't exist. This illustrates the primary principle of Test-Driven Development: don't write any code unless you have a test that fails. Remember, when your test code won't compile, that counts as a failing test. Now I create my BankAccount class and I write just enough code to make the tests compile:
namespace UnitTestingExamples.Library
{
using System;
public class BankAccount
{
public void Deposit( double amount )
{
}
public double Balance
{
get { return 0.0; }
}
}
}
This time everything compiles just fine, so I go ahead and run the test. My test fails with the message "TestDeposit: expected: <150> but was <0>". So the next thing we do it write just enough code to make this test pass:
namespace UnitTestingExamples.Library
{
using System;
public class BankAccount
{
private double _balance = 0.0;
public void Deposit( double amount )
{
_balance += amount;
}
public double Balance
{
get { return _balance; }
}
}
}
namespace UnitTestingExamples.Tests
{
using DotNetMock;
using System;
[TestFixture]
public class ModelTests
{
[Test]
public void TestSave()
{
MockDatabase db = new MockDatabase();
db.SetExpectedUpdates(2);
ModelClass model = new ModelClass();
model.Save( db );
db.Verify();
}
}
}
As you can see, the MockDatabase was easy to setup and allowed us to confirm that the Save method made certain calls on it. Also notice that the mock object prevents us from having to worry about real databases. We know that when the ModelClass saves itself, it should call the database's Update method twice. So we tell the MockDatabase to expect two updates calls, call Save and the confirm that what we expected really happened. Because the MockDatabase doesn't really connect to a database, we don't have to worry about keeping "test data" around. We only test that the Save code causes two updates.
[TestFixture]
public class VitalsControllerTests
{
[Test]
public void TestSuccessful()
{
MockVitalsView view = new MockVitalsView();
VitalsController controller = new VitalsController(view);
view.Name = "Peter Provost";
view.SSN = "123-45-6789";
Assertion.Assert( controller.OnOk() == true );
}
[Test]
public void TestFailed()
{
MockVitalsView view = new MockVitalsView();
VitalsController controller = new VitalsController(view);
view.Name = "";
view.SSN = "123-45-6789";
view.SetExpectedErrorMessage( controller.ERROR_MESSAGE_BAD_NAME );
Assertion.Assert( controller.OnOk() == false );
view.Verify();
view.Name = "Peter Provost";
view.SSN = "";
view.SetExpectedErrorMessage( controller.ERROR_MESSAGE_BAD_SSN );
Assertion.Assert( controller.OnOk() == false );
view.Verify();
}
}
When we build this we receive a lot of compiler errors because we don't have either a MockVitalsView or a VitalsController. So let's write skeletons of those classes. Remember, we only want to write enough to make this code compile.
public class MockVitalsView
{
public string Name
{
get { return null; }
set { }
}
public string SSN
{
get { return null; }
set { }
}
public void SetExpectedErrorMessage( string message )
{
}
public void Verify()
{
throw new NotImplementedException();
}
}
public class VitalsController
{
public const string ERROR_MESSAGE_BAD_SSN = "Bad SSN.";
public const string ERROR_MESSAGE_BAD_NAME = "Bad name.";
public VitalsController( MockVitalsView view )
{
}
public bool OnOk()
{
return false;
}
}
Now our test assembly compiles and when we run the tests, the test runner reports two failures. The first occurs when TestSuccessful calls controller.OnOk, because the result is false rather than the expected true value. The second failure occurs when TestFailed calls view.Verify. Continuing on with our test-first paradigm, we now need to make these tests pass. It is relatively simple to make TestSuccessful pass, but to make TestFailed pass, we actually have to write some real code, such as:
public class MockVitalsView : MockObject
{
public string Name
{
get { return _name; }
set { _name = value; }
}
public string SSN
{
get { return _ssn; }
set { _ssn = value; }
}
public string ErrorMessage
{
get { return _expectedErrorMessage.Actual; }
set { _expectedErrorMessage.Actual = value; }
}
public void SetExpectedErrorMessage( string message )
{
_expectedErrorMessage.Expected = message;
}
private string _name;
private string _ssn;
private ExpectationString _expectedErrorMessage =
new ExpectationString("expected error message");
}
public class VitalsController
{
public const string ERROR_MESSAGE_BAD_SSN = "Bad SSN.";
public const string ERROR_MESSAGE_BAD_NAME = "Bad name.";
public VitalsController( MockVitalsView view )
{
_view = view;
}
public bool OnOk()
{
if( IsValidName() == false )
{
_view.ErrorMessage = ERROR_MESSAGE_BAD_NAME;
return false;
}
if( IsValidSSN() == false )
{
_view.ErrorMessage = ERROR_MESSAGE_BAD_SSN;
return false;
}
// All is well, do something...
return true;
}
private bool IsValidName()
{
return _view.Name.Length > 0;
}
private bool IsValidSSN()
{
string pattern = @"^\d{3}-\d{2}-\d{4}$";
return Regex.IsMatch( _view.SSN, pattern );
}
private MockVitalsView _view;
}
Let's briefly review this code before proceeding. The first thing to notice is that we haven't changed the tests at all (which is why I didn't even bother to show them). We did, however, make significant changes to both MockVitalsView and VitalsController. Let's begin with the MockVitalsView. In our previous example, MockVitalsView didn't derive from any base class. To make our lives easier, we changed it to derive from DotNetMock.MockObject. The MockObject class gives us a stock implementation of Verify that does all the work for us. It does this by using expectation classes through which we indicate what we expect to happen to our mock object. In this case our tests are expecting specific values for the ErrorMessage property. This property is a string, so we add an ExpectationString member to our mock object. Then we implement the SetExpectedErrorMessage method and the ErrorMessage property to use this object. When we call Verify in our test code, the MockObject base class will check this expectation and identify anything that doesn't happen as expected. Pretty cool, eh? The other class that changed was our VitalsController class. Because this is where all the working code resides, we expected there to be quite a few changes here. Basically, we implemented the core logic of the view in the OnOk method. We use the accessor methods defined in the view to read the input values, and if an error occurs, we use the ErrorMessage property to write out an appropriate message. So we're done, right? Not quite. At this point, all we have is a working test of a controller using a mock view. We don't have anything to show the customer! What we need to do is use this controller with a real implementation of a view. How do we do that? The first thing we need to do is extract an interface from MockVitalsView. A quick look at VitalsController and VitalsControllerTests shows us that the following interface will work.
public interface IVitalsView
{
string Name { get; set; }
string SSN { get; set; }
string ErrorMessage { get; set; }
}
After creating the new interface, we change all references to MockVitalsView to IVitalsView in the controller and we add IVitalsView to the inheritance chain of MockVitalsView. And, of course, after performing this refactoring job we run our tests again. Assuming everything is fine, we can create our new view. For this example I will be creating an ASP.NET page to act as the view, but you could just as easily create a Windows Form. Here is the .ASPX file:
<%@ Page language="c#" Codebehind="VitalsView.aspx.cs"
AutoEventWireup="false"
Inherits="UnitTestingExamples.VitalsView" %>
<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.0 Transitional//EN" >
<html>
<head>
<title>VitalsView</title>
<meta name="GENERATOR" Content="Microsoft Visual Studio 7.0">
<meta name="CODE_LANGUAGE" Content="C#">
<meta name=vs_defaultClientScript content="JavaScript">
<meta name=vs_targetSchema
content="http://schemas.microsoft.com/intellisense/ie5">
</head>
<body MS_POSITIONING="GridLayout">
<form id="VitalsView" method="post" runat="server">
<table border="0">
<tr>
<td>Name:</td>
<td><asp:Textbox runat="server" id=nameTextbox /></td>
</tr>
<tr>
<td>SSN:</td>
<td><asp:Textbox runat="server" id=ssnTextbox /></td>
</tr>
<tr>
<td> </td>
<td><asp:Label runat="server" id=errorMessageLabel /></td>
</tr>
<tr>
<td> </td>
<td><asp:Button runat="server" id=okButton Text="OK" /></td>
</tr>
</table>
</form>
</body>
</html>
And here is the code-behind file:
using System;
using System.Web.UI.WebControls;
using UnitTestingExamples.Library;
namespace UnitTestingExamples
{
/// <summary>
/// Summary description for VitalsView.
/// </summary>
public class VitalsView : System.Web.UI.Page, IVitalsView
{
protected TextBox nameTextbox;
protected TextBox ssnTextbox;
protected Label errorMessageLabel;
protected Button okButton;
private VitalsController _controller;
private void Page_Load(object sender, System.EventArgs e)
{
_controller = new VitalsController(this);
}
private void OkButton_Click( object sender, System.EventArgs e )
{
if( _controller.OnOk() == true )
Response.Redirect("ThankYou.aspx");
}
#region IVitalsView Implementation
public string Name
{
get { return nameTextbox.Text; }
set { nameTextbox.Text = value; }
}
public string SSN
{
get { return ssnTextbox.Text; }
set { ssnTextbox.Text = value; }
}
public string ErrorMessage
{
get { return errorMessageLabel.Text; }
set { errorMessageLabel.Text = value; }
}
#endregion
#region Web Form Designer generated code
override protected void OnInit(EventArgs e)
{
//
// CODEGEN: This call is required by the ASP.NET Web Form Designer.
//
InitializeComponent();
base.OnInit(e);
}
/// <summary>
/// Required method for Designer support - do not modify
/// the contents of this method with the code editor.
/// </summary>
private void InitializeComponent()
{
this.Load += new System.EventHandler(this.Page_Load);
okButton.Click += new System.EventHandler( this.OkButton_Click );
}
#endregion
}
}
As you can see, the only code in the view is code that ties the IVitalsView interface to the ASP.NET Web Controls and a couple of lines to create the controller and call its methods. Views like this are easy to implement. Also, because all of the real code is in the controller, we can feel confident that we are rigorously testing our code.
General
News
Question
Answer
Joke
Rant
Admin
|
PermaLink |
Privacy |
Terms of Use
Last Updated: 17 Mar 2003 Editor: Chris Maunder |
Copyright 2003 by Peter Provost Everything else Copyright © CodeProject, 1999-2009 Web12 | Advertise on the Code Project |