Test Driven Development with ASP.NET

Change the Way You Develop Code

asp:cover story

LANGUAGES: VB.NET

ASP.NET VERSIONS: ALL

 

Test Driven Development with ASP.NET

Change the Way You Develop Code

 

By Ian Blackburn

 

Test Driven Development (TDD) is gaining in popularity as project managers and developers start to realize the benefits of producing verifiable, reliable, and robust code using this methodology and related techniques. This article will show you how to start using TDD for ASP.NET projects and how those benefits can be realized.

 

TDD is essentially about writing tests for your code, so that your code is verifiably correct. Many developers unfamiliar with TDD often think that it is in some way related to QA, or user acceptance testing (UAT). Or they may feel they already test their code and are not sure what TDD can offer. TDD can improve your code quality, and it can make UAT easier to manage but it is a bigger change than that. It is really a change in thinking and process that most developers, once they have tried in earnest, think is a better way of developing code.

 

What Is TDD?

TDD has two rules, as defined by Kent Beck in Test Driven Development: By Example (Addison-Wesley, 2003) and referenced in the excellent Test-Driven Development in Microsoft .NET by James W. Newkirk and Alexei A. Vorontsov (Microsoft Press, 2004):

1)     Never write a single line of code unless you have a failing automated test.

2)     Eliminate duplication.

 

Let s examine the first rule: Never write a single line of code unless you have a failing automated test. What that means, in part, is that we write our tests before we write any code. That s a novel approach for most developers, though they may already go through something like the test process we will examine albeit informally and at a different time.

 

For example, if you were asked to write a Math object that had an AddTwoNumbers method, you would probably have a go at writing the method and then start thinking about how you could check if AddTwoNumbers worked. You might think, Well, obviously if 2 and 2 are passed, I should get a 4. But you might also think about what should happen if negatives or nulls were passed. These thoughts would shape the code you write. What should happen if a null is passed? You might be able to refer to the specification for the answer, but often these sorts of questions are too detailed to be in there. So you make a choice and throw an exception. You write the code, then you run it and you try your various tests passing 2 and 2, passing a null, passing negative numbers and determine if the code performs as you expect. Now you need to document the choice you made, so you add a comment to the code, or maybe you go back and modify a specification document.

 

That s all fine, although we are writing comments and documentation (which is typically not your average developer s favorite task) to support our decisions. There is nothing that really cements that decision to the code, and we have to make sure the documentation is kept up to date if anything changes. Moreover, it is quite difficult to verify that our code has met the requirements we have established. We could use the user interface we developed (if we have it yet), or we could debug through the code, or we could set up our own little test page. But this might vary from one developer to another, or it might not happen at all until later on when the QA team is testing a bigger part of the functionality of the application. And, of course, if we have a bug, the longer we leave it, the more costly it becomes (a typo costs nothing to correct now, but if we leave it until the product is deployed/released there is most definitely a cost to bear). If we change something of the implementation, and the documentation gets out of date (I challenge anyone to say they have never had to put up with out-of-date documentation at some point in their career), then things could certainly get tricky. Someone else might be writing code that is calling our AddTwoNumbers method and expecting that exception, but now we are not providing it and a bug has been born that could possibly only show up in user acceptance testing sometime in the future.

 

Without using the TDD approach, it is very easy to become less than confident that making a code change in one area of the application won t have undesired effects in other areas.

 

With the TDD approach, you take a more formal approach to the tests we do to check if our code works as expected. And instead of documenting our decisions in Word or as comments, the decisions are documented in the tests we write. These tests are written in .NET using your normal development environment, such as Visual Studio, using whatever .NET language you want. Tests are automated because they can run automatically; we ll be using an open source application called NUnit (http://www.nunit.org) to do this, although there are others, and Microsoft is including a test suite that includes unit testing in the forthcoming Microsoft Team System 2005 (unit testing is another name for what is termed programmer tests in this article).

 

Tests can t become out of date because our test suite would fail, and it would be obvious we had a problem we would get a red bar (read on to find out about red and green bars).

 

The TDD process goes as follows:

1)     We write the tests we need; this essentially becomes the documentation for our code. We write our tests first because this forms the thinking on how to write the code.

2)     We write just enough code to compile, then we run the tests and see them fail. OK, this may sound a bit daft, but the point is to work in small chunks so that when anything fails we know exactly what it was.

3)     We write code as simply as possible to make the tests pass. As Einstein said, Do the simplest thing that works, but no simpler.

4)     Periodically we review our code and refactor the code to remove duplication. This simple process will drive your code to a good design. We can refactor as much as we like, and as long as the tests still pass, we know we haven t broken anything.

 

It Begins with Your Customers

TDD begins with your customers. These are the people, of course, who define the requirements for your application. In TDD, functional requirements are specified with user stories (see http://www.userstories.com and http://www.extremeprogramming.org/rules/userstories.html for more details). Let s take a simple example: an application to keep track of pocket money, which we are going to name KidsCash . The user stories for this application are as follows:

  • A user can create an account with a name, e-mail, and password.
  • A user can log in to an account with a name and password.
  • A user can delete their account (once they are logged in).
  • A user can deposit money into the account, with the date recorded and a note about the deposit.
  • A user can withdraw money up to the credit amount of the account (i.e., not go overdrawn), with the date recorded and a note about the deposit.
  • A user can see their balance at any time.
  • A user can see a history of their transactions at any time.

 

From this we could start thinking about our application design and architecture. We may decide to produce some UML diagrams, or we may go hunting for patterns and practices that would be appropriate. We could produce a technical specification detailing the objects that we ll need and the members they could contain. But this is not the TDD way. TDD means you write tests first, before the design, before any code is written. In TDD we write tests, see them fail, write code that makes them work, and then refactor the code to remove duplication. It is remarkable how this simple process produces good object-orientated design and robust code with far fewer bugs than normal.

 

So in TDD, after we have our first user stories, we can think about what tests we need to meet these requirements. It is often easier to think first about the way the application could be tested from the user s perspective. These are called customer tests, and are at a higher level and are less granular than the programmer tests we ll see later. These would be worked through with any customer who typically has input on what are the important areas to test. Customer tests can be automated (a tool for this to use with ASP.NET is NunitAsp; visit http://nunitasp.sourceforge.net/ for details), or they may be processed manually by your QA team and/or your customer. Here is a potential list of customer tests for our user stories :

 

Create Account, Login/Logout, Delete Account

1)     Go to the create account screen and create a new account with a name, e-mail, and password. If this succeeds you will be logged into the system.

2)     Log out of the system.

3)     Try to log in with the name and password used in step 1.

4)     Go to the create account screen and try to create a new account with the same name, e-mail, and password you used in step 1. You should get an error saying those details are already registered.

5)     Log in to the account using the details you created in step 1.

6)     Delete the account. This will also log you out.

7)     Try to log in with the name and password used in step 1. This should fail.

 

Deposit, Withdraw, Check Balance, and View History

1)     Create a new account with a name, e-mail, and password. This will also log you in.

2)     Check your balance; it should be zero.

3)     Deposit 50.23 with a date of 9 Jan 2003 and a note: Birthday Gift from Aunty Ros .

4)     Check your balance; it should be 50.23.

5)     Try to Deposit -10 with any date and note. This should not be allowed because you can t deposit negative numbers.

6)     Withdraw 10.12 with a date of 20 Jan 2003 and a note: Spiderman toy .

7)     Check your balance; it should be 40.11.

8)     Withdraw -5 with any date and note. This should not be allowed because you can t withdraw negative amounts.

9)     Check your balance; it should be 40.11.

10) Withdraw 50 with any date and note. This should not be allowed because you will be overdrawn.

11) Check your balance; it should be 40.11.

12) View the history of your account transactions. It should show two entries: 50.23 with a date of 9 Jan 2003 and a note: Birthday Gift from Aunty Ros ; and 10.12 with a date of 20 Jan 2003 and a note: Spiderman toy .

13) Delete the account. This will also log you out.

 

This is a great start for our application, but we need to turn these customer tests into code. What is the best way to achieve this? If you feel the urge to start thinking about an Account class and the members in it, stop! Instead, we are going to break these down into fine-grained programmer tests. The tests we come up with, typically in a brainstorm type session, will guide the code that we write. Here are the programmer tests we came up with for the KidsCash application:

 

Account Tests

  • Create an account with a name, e-mail, and password, get the new AccountID (auto-generated by the db) and assert all the details are in the database.
  • Get an account using a non-existent AccountID and assert ExpectedException.
  • Attempt to create an account with an existing name and assert ExpectedException.
  • Authenticate with correct credentials and assert a successful login.
  • Authenticate with incorrect credentials and assert login has failed.
  • Log in and assert the correct account details are retrieved.
  • Create a new account and assert the balance is 0.
  • Delete an account and assert it is no longer in the database.
  • Log out of an account and assert no account details can be accessed.

 

Deposit Tests

  • Deposit a positive amount and assert the balance has increased correctly.
  • Deposit a positive amount and assert the date and note are stored correctly.
  • Deposit a positive amount and assert the transaction history includes the deposit details correctly.
  • Deposit a negative amount and assert this fails.
  • Deposit a null amount and assert this fails.
  • Deposit 0 and assert this fails.

 

Withdraw Tests

  • Withdraw a positive amount and assert the balance has decreased correctly.
  • Withdraw a positive amount and assert the date and note are stored correctly.
  • Withdraw a positive amount and assert the transaction history includes the withdrawal details correctly.
  • Withdraw a negative amount and assert this fails.
  • Withdraw a null amount and assert this fails.
  • Withdraw 0 and assert this fails.
  • Withdraw more than the balance and assert this fails.

 

We are now ready to start coding, but first we need to explain how to use NUnit.

 

Using NUnit

You can get the latest version of NUnit from http://www.nunit.org (this article is based on version 2.1.4). It is freely available and easy to install simply run setup. It won t impact anything else you have on your system, as far as I am aware.

 

After installation you will have a program group with various samples and documentation links, and a shortcut to Nunit-Gui (which is what we ll use later to run our tests). It also creates a folder (default C:\Program Files\NUnit V2.1\bin) containing various assemblies. The one we are interested in is nunit.framework.dll, which we ll use to create our test suite.

 

Let s get coding! Create a new Visual Basic ASP.NET Web application called KidsCash, then add a reference to nunit.framework.dll (default C:\Program Files\NUnit V2.1\bin). Create a class called KidsAccount and add the following Imports statement at the top of the file:

 

Imports NUnit.Framework

 

We are going to create our tests before we write any code. You can create your tests in the same file or project as the actual code, or you can create a separate assembly with them in it. I like to put the test code in the same file or same project, then use conditional compilation to make sure it is not included in the production site. For me, having the tests near the code just feels easier to work with.

 

In the KidsAccount class create a new class called KidsAccountTest, and use the TestFixture attribute to identify it as a test suite, as shown here:

 

_

Public Class KidsAccountTest

End Class

 

The first programmer test we identified was:

  • Create an account with a name, e-mail, and password, get the new AccountID (auto-generated by the db) and assert all the details are in the database.

 

A programmer test for this is shown in Figure 1.

 

_

Public Sub CreateAccount()

 Dim ka As New KidsAccount()

 Dim accountID As Integer

 Dim name As String = "Rebecca"

 Dim email As String = "[email protected]"

 Dim password As String = "password"

 accountID = ka.CreateAccount(name, email, password)

 ka.GetAccount(accountID)

 Assert.AreEqual(name, ka.Name)

 Assert.AreEqual(password, ka.Password)

 Assert.AreEqual(email, ka.Email)

End Sub

Figure 1: The first programmer test we came up with for the KidsCash application.

 

Notice that we use the attribute to identify this as a test. When we use Nunit-Gui later, it will use this attribute to find all the tests in our assembly and run them.

 

NUnit provides the Assert class, which we are using here to perform our test. In this instance, we are using the AreEqual method, which compares an actual value with an expected value and returns true if they are the same, or false if not.

 

Of course, if you type in the code shown in Figure 1 it will not compile, because our KidsAccount class does not yet have a CreateAccount method, a GetAccount method, or the Name, Password, Email, and AccountID properties. Remember, in TDD we do the simplest thing that works. Let s add just enough code so that we can compile, and then we can run our test. Figure 2 shows the KidsAccount class with just enough code to compile.

 

Public Class KidsAccount

 Public Name As String

 Public Password As String

 Public Email As String

 Public AccountID As Integer

 Public Function CreateAccount(name As String,

   email As String, password As String) as Integer

      Return 0

 End Function

 Public Sub GetAccount(accountID As Integer)

 End Sub

End Class

Figure 2: The KidsAccount class with just enough code to compile.

 

Make sure you have compiled your application, then start Nunit-Gui from your Start menu. We need to begin a new project in Nunit-Gui, so select File | New Project and name it KidsCash. It is easiest to save your project file in the root of your Web site, because it is easy to find config files and any other files to which we need a relative path. We need to add our assembly to this project, so select Project | Add Assembly, then find and add the KidsCash.dll you created. Finally, we need to tell Nunit-Gui what our config file is named. We haven t used a config file in our application yet, but it is worth setting this up now so that it is ready. Select Project | Edit, then change the Configuration File Name to web.config.

 

To see if your tests pass, click the Run button in Nunit-Gui. Of course the test fails and we get a red bar; we didn t expect anything else. Figure 3 shows the result of running the test.

 


Figure 3: In this case failure was not only an option, it was expected.

 

At this stage we have a failing test that we need to make work. You now write the code to fix this. You can have a go yourself or download the sample (the sample also includes a SQL script to set up a SQL Server database to use; see end of article for download details). After this first test has passed and you have a green bar in Nunit-Gui (see Figure 4), you can move on to the next test. This first test is a great start. Being able to run this again at any time in the future to ensure you have no breaking changes from other code you write gives you great confidence in your work.

 


Figure 4: Passing the first test gives you the green light to proceed.

 

Continuing the Process

In the TDD process, you continue working through the programmer tests, doing just enough to make them pass so that you get the green bar. As you do this, you ll no doubt spot duplicated code, and you ll want to refactor that to remove the duplication (refactoring simply means reorganizing your code so that it is better; a good example of which is extracting code that is duplicated in more than one procedure and putting it into its own procedure). That s fine; go ahead and refactor as and when required. As long as you continue getting a green bar in your tests, everything is fine. However, be careful not to refactor just to get a test to work, or to modify a test because of refactoring. Testing and refactoring should be separate tasks, and you should do them individually.

 

You may also spot additional tests that you should have included; feel free to add these as required. Each test should be as small as is reasonably possible, so that you are testing one thing at a time.

 

Test code may also need refactoring. In particular, you often need to set up the environment before running a test, then tear it down afterward. NUnit provides attributes to make this easy. Figure 5 shows how this was achieved for the KidsCash project. It is important to realize that the SetUp and TearDown procedures run immediately before and after, respectively, every test in the class. In Figure 5, we set up an account before each test and then delete it after each test.

 

Private ka As New KidsAccount()

Private accountID As Integer

Private name As String = "Rebecca"

Private email As String = "[email protected]"

Private password As String = "password"

_

Public Sub SetUp()

 accountID = ka.CreateAccount(name, email, password)

 ka.GetAccount(accountID)

End Sub

_

Public Sub TearDown()

 ka.DeleteAccount(accountID)

End Sub

Figure 5: NUnit attributes make it easy to set up an account before each test and then delete it afterward.

 

Sometimes you need a test where you would expect an exception to be generated. NUnit deals with this scenario using the ExpectedException attribute. For example, we have a programmer test that states: Get an account using a non-existent AccountID and assert ExpectedException . We can write it like this:

 

 ArgumentOutOfRangeException))> _

   Public Sub GetAccountWithBadAccountID()

     ka.GetAccount(-1)

 End Sub

 

The Assert object has other methods in addition to the AreEqual one we saw previously; for example, the IsTrue method used in the LoginTest:

 

_

Public Sub LoginTest()

   Assert.IsTrue(ka.Authenticate(name, password, accountID))

End Sub

 

Have a look at the sample code for other examples, as well as the NUnit documentation.

 

Conclusion

In this article we introduced the concepts of Test Driven Development and how you can apply TDD to an ASP.NET project (the sample ASP.NET project is available for download; it includes all the Account tests for you to try). We examined a small but relevant example, working through the process of producing programmer tests and then beginning a test suite using NUnit as our test framework that will give us repeatable, verifiable code for our application. This is going to mean we have greater confidence in making changes to code, and, ultimately, that we have less bugs.

 

In a future article I ll explore the use of mock objects in Unit Testing, together with refactoring techniques, as well as ways to test the user interface for ASP.NET projects.

 

The sample code referenced in this article is available for download.

 

Ian Blackburn is technical director of Blackburn IT Services Ltd. (http://www.bbits.co.uk). bbits offers specialized .NET training, development, and consultancy in the UK. Ian has worked with a variety of clients, from small start-up companies to blue-chip companies and prestigious institutions such as the House of Lords. Ian has authored many articles and books and is available for technical consultancies and presentations worldwide. You can reach Ian at mailto:[email protected].

 

 

 

Hide comments

Comments

  • Allowed HTML tags: <em> <strong> <blockquote> <br> <p>

Plain text

  • No HTML tags allowed.
  • Web page addresses and e-mail addresses turn into links automatically.
  • Lines and paragraphs break automatically.
Publish