Using Test-Driven Development and the Red-Green-Refactor Workflow
With test-driven development (TDD), you use unit tests to help design your code. This can be an odd
concept if you are used to testing after you have finished coding, but there is a lot of sense in this
approach. The key concept is a development workflo w called red-green-refactor. It works like this:
1. Determine that you need to add a new feature or method to your application.
2. Write the test that will validate the behavior of the new feature when it is
written.
3. Run the test and get a red light.
4. Write the code that implements the new feature.
5. Run the test again and correct the code until you get a green light.
6. Refactor the code if required. For example, reorganize the statements, rename
the variables, and so on.
7. Run the test to confirm that your changes have not changed the behavior of
your additions.
This workflow is repeated fo r every feature you add.
Let’s walk through an example so you can see how it works. Let’s imagine the behavior we want is
the ability to add a bid to an item, but only if the bid is higher than all previous bids for that item. First,
we will add a stub method to the Item class, as shown in Listing 4-8.
Listing 4-8. Adding a Stub Method to the Item Class
using System;
using System.Collections.Generic;
namespace TheMVCPattern.Models {
public class Item {
public int ItemID { get; private set; } // The unique key
public string Title { get; set; }
public string Description { get; set; }
public DateTime AuctionEndDate { get; set; }
public IList<Bid> Bids { get; private set; }
public void AddBid(Member memberParam, decimal amountParam) {
throw new NotImplementedException();
}
}
}
It’s obvious that the AddBid method, shown in bold, doesn’t display the required behavior, but we
n’t let that stop us. The key to TDD is to test for the correct behavior before implementing the feature.
are going to test for three different aspects of the behavior we are seeking to implement:
• When there are no bids, any bid value can be added.
• When there are existing bids, a higher value bid can be added.
• When there are existing bids, a lower value bid cannot be added.
To do this, we create three test methods, which are shown in Listing 4-9.
Listing 4-9. Three Test Fixtures
[TestMethod()]
public void CanAddBid() {
// Arrange - set up the scenario
Item target = new Item();
Member memberParam = new Member();
Decimal amountParam = 150M;
// Act - perform the test
target.AddBid(memberParam, amountParam);
// Assert - check the behavior
Assert.AreEqual(1, target.Bids.Count());
Assert.AreEqual(amountParam, target.Bids[0].BidAmount);
}
[TestMethod()]
[ExpectedException(typeof(InvalidOperationException))]
public void CannotAddLowerBid() {
// Arrange
Item target = new Item();
Member memberParam = new Member();
Decimal amountParam = 150M;
// Act
target.AddBid(memberParam, amountParam);
target.AddBid(memberParam, amountParam - 10);
}
[TestMethod()]
public void CanAddHigherBid() {
// Arrange
Item target = new Item();
Member firstMember = new Member();
Member secondMember = new Member();
Decimal amountParam = 150M;
// Act
target.AddBid(firstMember, amountParam);
target.AddBid(secondMember, amountParam + 10);
// Assert
Assert.AreEqual(2, target.Bids.Count());
Assert.AreEqual(amountParam + 10, target.Bids[1].BidAmount);
}
We’ve created a unit test for each of the behaviors we want to see. The test methods follow the
arrange/act/assert pattern to create, test, and va lidate one aspect of the overall behavior. The
CannotAddLowerBid method doesn’t have an assert part in the method body because a successful test is an
exception being thrown, which we assert by applying the ExpectedException attribute on the test method.
As we would expect, all of these tests fail when we run them, as shown in Figure 4-10.
Figure 4-10. Running the unit tests for the first time
We can now implement our first pass at the AddBid method, as shown in Listing 4-10.
Listing 4-10. Implementing the AddBid Method
using System;
using System.Collections.Generic;
namespace TheMVCPattern.Models {
public class Item {
public int ItemID { get; private set; } // The unique key
public string Title { get; set; }
public string Description { get; set; }
public DateTime AuctionEndDate { get; set; }
public IList<Bid> Bids { get; set; }
public Item() {
Bids = new List<Bid>();
}
public void AddBid(Member memberParam, decimal amountParam) {
Bids.Add(new Bid() {
BidAmount = amountParam,
DatePlaced = DateTime.Now,
Member = memberParam
});
}
}
}
We’ve added an initial implementation of the AddBid method to the Item class. We’ve also added a
simple constructor so we can create instances of Item and ensure that the collection of Bid objects is
properly initialized. Running the unit tests again generates better results, as shown in Figure 4-11.
Two of the three unit tests have passed. The one that has failed is CannotAddLowerBid. We didn’t add
any checks to make sure that a bid is higher than previous bids on the item. We need to modify our
mplementation to put this logic in place, as shown in Listing 4-11.
Listing 4-11. Improving the Implementation of the AddBid Method
using System;
using System.Collections.Generic;
using System.Linq;
namespace TheMVCPattern.Models {
public class Item {
public int ItemID { get; private set; } // The unique key
public string Title { get; set; }
public string Description { get; set; }
public DateTime AuctionEndDate { get; set; }
public IList<Bid> Bids { get; set; }
public Item() {
Bids = new List<Bid>();
}
public void AddBid(Member memberParam, decimal amountParam) {
if (Bids.Count() == 0 || amountParam > Bids.Max(e => e.BidAmount)) {
Bids.Add(new Bid() {
BidAmount = amountParam,
DatePlaced = DateTime.Now,
Member = memberParam
});
} else {
throw new InvalidOperationException("Bid amount too low");
}
}
}
}
You can see that we have expressed the error condition in such a way as to satisfy the unit test we
wrote before we started coding; that is, we throw an InvalidOperationException when a bid is received
that is too low.
Each time we change the implementation of the AddBid method, we run our unit tests again. The
results are shown in Figure 4-12.
Success! We have implemented our new feature such that it passes all of the unit tests. The last step
is to take a moment and be sure that our tests really do test all aspects of the behavior or feature we are
implementing. If so, we are finished. If not, then we add more tests and repeat the cycle, and we keep
going until we are confident that we have a comprehensive set of tests and an implementation that
passes them all.
This cycle is the essence of TDD. There is a lot to recommend it as a development style, not least
because it makes a programmer think about how a change or enhancement should behave before the
coding starts. You always have a clear end point in view and a way to check that you are there. And if you
have unit tests that cover the rest of your application, you can be sure that your additions have not
changed the behavior elsewhere.