VSM Cover Story

Test Your .NET 3.5 Apps

Take advantage of the tools built into VS.NET 2008 to automate testing your application logic. Also, learn how to design your applications so you can take advantage of unit testing and test-driven development methodologies.

TECHNOLOGY TOOLBOX: VB.NET, C#, SQL Server , ASP.NET

When developing software, you must test your programs to ensure that all the business logic works correctly, and that it meets customer requirements. Such testing is generally known as "unit testing" because the tests are supposed to exercise the smallest units of code within each program. A "unit" of code might be alibrary, sub-routine, function, or procedure. In .NET, and in object-oriented programming in general, the common "unit" of code is the class.

Unit testing isn't a new idea; unit tests have been around in one form or another since the inception of computer programming. A unit test, in its simplest form, is executed by operating a program manually and invoking functions or methods from the console or command line. Often, programmers create test harnesses -- programs that exercise the functions of another program or library. However, with the advent of modern, object-oriented programming and standardized unit testing frameworks, unit testing has improved significantly. Creating unit tests is no longer a laborious chore; rather, creating them is now an integral part of the development process, and developers are able to leverage "test-driven" or "test-first" development practices to create robust, object-oriented code in a way that verifies that business requirements are met.

In this article, we'll show you how to use the tools in Visual Studio .NET 2008 to automate the testing of your application's logic. We'll also show you how to design your applications so you can take advantage of unit testing and test-driven development methodologies.This online version of the article contains both C# and VB.NET code samples, and you can also get both by downloading the code sample here.

VS.NET 2008 ships with a comprehensive suite of testing tools that facilitate automating the testing of your applications. The addition of VS.NET Team Suite means there are now even more tools available. But if you're using the Professional Edition (which most of us are) the tools included are still more than adequate to meet the needs of most development situations. The basic testing tools include a wizard that creates tests automatically for each method in your classes; custom attributes to identify test classes and test methods; an Assert class with static methods to test for the success or failure of a test case; the ability to run single or multiple tests; and the ability to run tests in a specific order. Another nice feature about these tools is that test projects store data and metadata about the assemblies being tested, and they include both a graphical user interface and a console interface to display progress and results of tests.

Create Your First Test
It's probably best to start out with a simple introduction to running tests in VS.NET 2008. Start up a new instance of VS.NET 2008 and create a class library project named MyClasses. Next, add a new class named FileProcess to this library:

// C#
using System.IO;

public class FileProcess
{
  public bool FileExists(string FileName)
  {
    return File.Exists(FileName);
  }
}

' VB.NET
Imports System.IO

Public Class FileProcess
  Public Function FileExists(ByVal FileName As String) _
    As Boolean
    Return File.Exists(FileName)
  End Function
End Class

Once you create this class with the method called FileExists, you're ready to create a test for it. From the VS.NET menu, choose Test > New Test…, and click on the Unit Test Wizard option. From the "Add to test project" drop-down list, choose "Create a new Visual Basic test project" or "Create a new C# test project," and click on the OK button. A dialog will now prompt you for the name of the new project. Set the name to FileProcessTest and click on the Create button. From the Create Unit Tests dialog, drill down to the FileExists method and place a check mark next to that method. Click on the OK button and VS.NET 2008 will create a new project with a unit test already generated for this method.

Your new test class is now generated and the FileExists method has a unit test method called FileExistsTest:

// C#
[TestMethod()]
public void FileExistsTest()
{
  FileProcess target = new FileProcess();
  string FileName = string.Empty;
  bool expected = false;
  bool actual;

  actual = target.FileExists(FileName);
  Assert.AreEqual(expected, actual);
}

' VB.NET
<TestMethod()> _
Public Sub FileExistsTest()
  Dim target As FileProcess = New FileProcess
  Dim FileName As String = String.Empty
  Dim expected As Boolean = False
  Dim actual As Boolean

  actual = target.FileExists(FileName)
  Assert.AreEqual(expected, actual)

End Sub

Let's take a moment to discuss what this code is doing. The variable declarations are easy to understand; the test harness creates a variable for each parameter and the return value from the method. The variable target will be an instance of the class you'll test. The variable actual is the return value from the method. Once you setup the variables correctly and the method is called, you use the Assert.AreEqual method to compare the variable expected to the value returned (actual). The Assert class communicates the success or failure of the test to the testing framework by using various methods in the Assert class. Check the online VS.NET documentation for a list of the many Assert class methods you can take advantage of.

Prior to running the test, you need to configure the various variables in the test to values that will allow the test to succeed. First, change the FileName variable to a file name instead of just an empty string. You should also change the expected variable to what you expect to be returned from this method. Modify the variable declarations in this unit test to look like this:

// C#
string FileName = @"D:\Test.txt";
bool expected = true;

' VB.NET
Dim FileName As String = "D:\Test.txt"
Dim expected As Boolean = True

You're now ready to run this test and verify the results. From the VS.NET menu, choose Test > Run > Tests in Current Context. This causes all the tests in this class to be executed. The results of the test are displayed in the Test Results window (see Figure 1). The test fails because presumably the file name you're checking for doesn't exist yet. If you go create that file and then run the test again, it will succeed.

Initialize and Clean up Tests
If you want to initialize some data prior to running your tests, you can use a specific method that has been decorated with the ClassInitialize attribute. You can find this method in the "Additional test attributes" #region in the FileProcessTest class. Uncomment and modify the MyClassInitialize method as shown below. You can also tear down your tests by using the MyClassCleanup method that is decorated with the ClassCleanup attribute. For purposes of this class, that means creating the file to test its existence, and then deleting it when the test is complete. Go ahead and uncomment the two methods and add the code shown in bold:

C#
[ClassInitialize()]
public static void MyClassInitialize(
  TestContext testContext)
{
  System.IO.File.AppendAllText(@"D:\Test.txt",
    "Some Content");
}
    
[ClassCleanup()]
public static void MyClassCleanup()
{
  System.IO.File.Delete(@"D:\Test.txt");
}

' VB.NET
<ClassInitialize()> _
Public Shared Sub MyClassInitialize( _
  ByVal testContext As TestContext)
  System.IO.File.AppendAllText( _
    "D:\Test.txt", "Some Content")
End Sub

<ClassCleanup()> _
Public Shared Sub MyClassCleanup()
  System.IO.File.Delete("D:\Test.txt")
End Sub

You often need to create methods that throw an exception if something in that method doesn't happen as expected. For example, create a method in the FileProcess class called FileExistsWithException. In this method, check to see whether the FileName parameter is passed in as a null or empty string. If it is, then throw an ArgumentNullException:

// C#
public bool 
  FileExistsWithException(
  string FileName)
{
  if (string.IsNullOrEmpty(
    FileName)) {
    throw new ArgumentNullException(
      "FileName");
  }
  else {
    return File.Exists(FileName);
  }
}

' VB.NET
Public Function _
  FileExistsWithException( _
  ByVal FileName As String) _
    As Boolean
  If String.IsNullOrEmpty( _
    FileName) Then
    Throw New _
      ArgumentNullException(_
      "FileName")
  Else
    Return File.Exists(FileName)
  End If
End Function

Use the same Unit Test Wizard described previously to generate a test for this new method. When the wizard asks where to place this new test, it should default to the FileProcessTest project. Make sure you choose that option, so the test is added to the same class as the other test. The code that's generated will not handle the exception automatically, so you must fix up this generated test code. Add the ExpectedException attribute to the test declaration as shown in bold:

// C#
[TestMethod(), ExpectedException(typeof(
  System.ArgumentNullException))]
public void FileExistsWithExceptionTest()
{
  …
}

' VB.NET
<TestMethod()> _
<ExpectedException(GetType( _
  System.ArgumentNullException))> _
Public Sub FileExistsWithExceptionTest()
  …
End Sub

You can run this test now, passing in a blank file name, and the test will still succeed because it expects this exception to be thrown. At this point, you have the basics of unit testing down. The next step is to turn our attention to testing .NET applications.

Create a Sample App
Let's take a fairly typical input form such as a TimeSheet Entry page (see Figure 2). The requirements for this entry form aren't as simple as they might initially appear. In fact, the total number of business rules on this simple little form might surprise you.

A simple form like this, with only five entry fields could include considerably more rules than fields. For example, a typical set of business rules might include these nine requirements:

  1. A Resource must be selected.
  2. A valid Entry Date must be entered.
  3. The Entry Date must be today's date or less.
  4. The Entry Date must be no older than seven days.
  5. Customer must be selected.
  6. The Hours must be in a decimal value format.
  7. The value for Hours must be between 1 and 12.
  8. A Description must be entered.
  9. The Description must have more than 10 characters.

Imagine how many more rules you would find if it were a much larger entry form.

Before I explain how to automate the rules, it might help to look at one way many developers might validate the rules on this form. When a user clicks on the Save button, you might write some code in the Save button's Click event:

// C#
protected void btnSubmit_Click(object sender, EventArgs e)
{
  if (ValidateData()) {
    lblMessage.Text = "Data is Valid";
  }
  else {
    lblMessage.Text = 
      mstrMsg.Replace(Environment.NewLine, "<br />");
  }
}

' VB.NET
Protected Sub btnSubmit_Click(ByVal sender As Object, _
 ByVal e As System.EventArgs) Handles btnSubmit.Click
  If ValidateData() Then
    lblMessage.Text = "Data is Valid"
  Else
    lblMessage.Text = _
     mstrMsg.Replace(Environment.NewLine, "<br />")
  End If
End Sub

Note that this code comes from a Web version of the form; the code for a Windows form version of this app would look nearly identical to this.

The ValidateData() method that you call from this btnSave_Click event procedure is where the validation of the business rules for this form takes place. This method returns a true or false to indicate that the business rules passed or failed the tests. You could write code that performs all your business rules validation based on the btnSave_Click event, and the online version of this article contains sample code that illustrates this technique, however, there are a few things wrong with this approach. First, the code is embedded in a form (the presentation layer). These rules cannot be reused if you move this application to an ASP.NET application, a Web Service, or a Windows Service. Second, if you copied this code to an ASP.NET application and the user adds a new business-rule requirement, the code must be changed in two places. Third, the only way to test this code is to execute the form manually and fill in various fields in such a way that it makes the form fail. Fourth, the only way to test that any new code introduced still meets all the business requirements is, again, by executing the form manually and trying all the different scenarios to test all the business rules. Obviously, the greater the number of business rules, the greater the number of permutations you must test.

To fix this form and solve all the identified problems, you need to take a more object-oriented approach to development. This means that you will have to remove the business rules from the form and place them into a class. This is not only a good idea, but an ideal approach for writing unit tests that can automate code testing.

Implement UI Testing
It would be nice if there were a tool that would automate the process of clicking on every link on a Web page and/or every button on a Windows form application; however, such tools are hard to find, generally expensive, and also costly in terms of time to setup and use. You also need to consider the regression testing that occurs when you add a new button and/or link. Someone has to remember to go in and add those to the testing process. This takes more time and more money to perform, and thus leads to more costs. There's also the danger that someone will forget to go back and add the additional tests.

Rather than trying to automate the testing of the UI layer, you should attempt to move all business logic into methods within classes. This will make automated unit testing much easier to accomplish. Let's explore how to convert this earlier form from one that contains UI business rules to one that contains business rules within a class. The goal here is to allow you to use the VS.NET 2008 automated testing framework to create unit tests for all business rules that are called from the UI layer. The first step is to re-architect your btnSave_Click event procedure and your ValidateData method in your form:

// C#
protected void btnSubmit_Click( 
  object sender, EventArgs e) {
  if (ValidateData()) {
    lblMessage.Text = "Data is Valid";
  }
}

private bool ValidateData() {
  TimeSheetSample3 ts = 
    new TimeSheetSample3();

  if (!ts.ValidateData(
    ddlResource.SelectedItem.Text,
      txtEntryDate.Text,
      ddlCustomer.SelectedItem.Text,
      txtHours.Text,
      txtDescription.Text)) {
    lblMessage.Text = 
      ts.MessagesForWebDisplay;
  }

  return (ts.Messages == string.Empty);
}


' VB.NET
Protected Sub btnSubmit_Click( _
  ByVal sender As Object, _
  ByVal e As System.EventArgs) _
  Handles btnSubmit.Click
  If ValidateData() Then
    lblMessage.Text = "Data is Valid"
  End If
End Sub

Private Function ValidateData() As Boolean
  Dim ts As New _
    TimeSheetComponentsVB.TimeSheetSample3()

  If Not ts.ValidateData( _
   ddlResource.SelectedItem.Text, _
   txtEntryDate.Text, _
   ddlCustomer.SelectedItem.Text, _
   txtHours.Text, _
   txtDescription.Text) Then
    lblMessage.Text = ts.MessagesForWebDisplay
  End If

  Return (ts.Messages = String.Empty)
End Function

The ValidateData method simply passes the data from the controls directly into a method in the TimeSheetSample3 class. This method accepts all string values, regardless of what the final data type should be. For example, the Entry Date should be a date, and the Hours should be a decimal data type. The ValidateData method takes care of accepting the string value directly from the form and validating that the data is the correct data type prior to checking the normal business rules. If you didn't do this, you would have to validate for dates and numeric values in the UI layer. For an example of the UI layer doing this type of validation, check out the online sample solution that accompanies this article and look at the frmSample2 and TimeSheetSample2 classes.

The advantage of creating a method that only accepts the string values from the UI layer and performs the validation within the class is two-fold. First, you eliminate business logic processing in the UI layer where it's harder to test. Second, you can now write unit tests to test all possible combinations of bad data getting passed to this method. For example, you can check for bad dates, bad numeric data, potential SQL injection attacks, and anything else you can think of. You can rewrite these tests at any time and re-execute them whenever a change is made or whenever you develop new tests.

For example, look at the code in Listing 1, paying particular attention to the logic in the ValidateData method. After the ValidateData method is called, it then calls a protected method -- also named ValidateData -- to perform the normal business rule checking.

The protected overload method named ValidateData performs the normal business rule checking after the public overload ValidateData checks the data for correct data types (see Listing 2). Both of these methods perform error checking, and you could combine them, but each uses different inputs. The public version uses string data and places the string data into the private variables of the TimeSheetSample3 class after the string data is validated. The protected ValidateData method uses the private variables that have been set with good data from the public ValidateData method. If errors are encountered in either method, then an appropriate error message is generated and appended to a string variable that can be reported back through a public variable on this class.

Testing for the Real World
Working on real-world projects for our customers reveals a few things that you can do to be more productive when working with unit tests and test-driven development. For example, a frequently asked question is: "What do I test?" Conversely, some developers ask: "What do I not test?"

Ideally, tests exercise every feature of a program. A program could include conditionals, loops, and methods; in addition, object-oriented software includes inheritance relationships. Unit tests should exercise all conditions and all branches of flow control, as well as differences in behavior due to inheritance.

In practice, there are two principles to adhere to when considering what to test and what not to test. First, test all code that you write. Second, don't write tests for third-party software or generated code. In the Timesheet Entry example, unit tests cover the ValidateData method and variations in the control flow of the ValidateData method to exercise validation and other business logic. You shouldn't produce tests for standard features of .NET, third-party controls, APIs, or generated code, unless the feature you're building that relies on third-party code results in unexpected or undocumented behavior under test conditions.

If you're now just getting into automated testing, you can follow the scenario outlined in this article to test your existing code. However, if you're developing a brand-new application, you might want to consider test-driven development.

Test-driven development is the process where you write your tests before you write the code. The tests dictate how you should write the code. Forcing yourself to think about the processes that have to happen to accomplish the goals for your application can help you write each method to be more robust. By looking at the test, you'll see the various inputs to the method, and you can write the method to ensure that it handles each of those inputs. The result is better code. In fact, the tests themselves become documentation for your application code.

VS.NET 2008 has added some nice new testing tools that will definitely help you test your applications better. Unit tests can now be automatically generated for your classes in your projects. UI testing is still not supported by the Professional Edition of VS.NET 2008, but some tools are available in VS.NET Team Studio Edition. You can also purchase some third-party tools to help you do UI testing. The best approach, however, is to move more of your business logic into classes and out of your UI layer.

comments powered by Disqus
Upcoming Events

.NET Insight

Sign up for our newsletter.

I agree to this site's Privacy Policy.