Published:
03 Mar 2009
|
Abstract
Many developers want to learn to use the practices of Test Driven Development. In this article, Brendan explains how to get started writing tests before writing code. He uses code snippets of tests and the implementation code to demonstrate some practices he uses when testing. |
|
by Brendan Enrick
Feedback
|
Average Rating:
Views (Total / Last 10 Days):
34765/
53
|
|
|
Introduction |
As a software developer, it is nearly impossible to miss all
the hype which has been revolving around Test Driven Development. Many people
and organizations are embracing this old, but newly popular, idea. There are
testing frameworks, mocking frameworks, IoC containers, and plenty of other
tools used by testers. There are a variety of different ways to go about
testing, so I will begin by stating that there are alternatives to what I
describe in this article. I am merely trying to explain using a style of
testing similar to what I use.
In this article I will be using a few tools that you will
want to have if you plan on following along. Alternatively, if you download the
sample application it is bundled such that the required DLLs are included with
the project.
Tools Used In the Article
·
Visual
Studio 2008 - Development Environment
·
RhinoMocks
- Mocking framework
·
NUnit -
Testing framework
|
Creating Tests First |
One comment that comes up often when people hear that they are
supposed to write a test before they write their code is, "that it is just
going to create a compiler error". Well, that is correct, but keep in mind
that I said there are a variety of ways to do things. You do not have to create
the tests in advance if you don't want to, but I suggest that you do. We have
at least a couple of reasons why we create the tests first; it helps ensure we
are testing the correct thing and it also gives the chance to come up with the
desired interface to use for the code you will be testing.
Ensuring the Test is Accurate
I will tell you, that you will, at some point, write a test
method which you expect to fail, but instead it passes. Plenty of reasons exist
which can cause this to occur, but at the end of the day it means that your
test is not accurately testing your code. All it takes to make a test pass by
mistake is; swapping a less than or greater than, forgetting an assert
statement, or sometimes things just do not work as you believe they do. Writing
the test first allows you to confirm your theory of how everything is working.
Creating the Desired Interface
Sometimes developers will spend a great deal of time trying
to figure out what interface they want to use for their code. How do you want
to interact and use some piece of code? Well, one of the best ways to figure it
out is to come up with a use case and attempt to use it. Tests make great use
cases; they demonstrate how to use a particular class or method, and what the
result of that usage is. So if we assume that the test uses our class, we are
able to come up with the interface we want to use to access the class before we
have written it. What does this mean? Well, it means that we now know how to
design the interaction, because we've just tried to use it.
If we did not do things this way, we would be designing
based on a guess of how we would want to interact with the code we are going to
create. Keep this in mind when judging the value of writing tests in advance of
code changes. I believe it is useful and helpful, but this is one of the points
where you can easily go either way.
|
Creating Two Simple Tests |
To start off, we want something easy just to demonstrate
what types of things we want to test, and how we go about testing them. Before
we dive into writing our first test, I should probably at least give a small
amount of background on our example application. We are going to be working on
a card game. For the purposes of this article, we will just be focusing our
attention on a few classes in the game; Deck, Card, and Player.
We should always start off with the simplest thing we want
to do. The first thing we need to do is come up with something our code should
do. We want to then write a test which asserts that our code works as expected.
Naming your tests is extremely important. If you don't name
your test well, you will not know what it is testing later and you might go off
on a tangent and test something entirely different. You want to be descriptive
enough to describe the problem, but you also need to be concise. For this first
test, we will use the name, DeckCountShouldEqualCardCount. We will follow that
test with one called, DeckShouldHaveOneLessCardAfterDrawing. Some people will
read these names and think I am crazy, but let me remind you that you will have
tests for every class and without names this descriptive; you will not know
what is wrong when tests fail. Having a descriptive name tells you immediately
what failed, because it is in the name
We know that we need to be able to draw cards and that after
we draw there should be one less card in the deck, so we should start off by
creating a deck with a collection of cards, keeping track of the number of
initial cards. We then draw a card from the deck and assert afterwards that the
number of cards in the deck has decreased by one. The following are our first
test methods.
Listing 1: The Simple Tests
[Test]
public void DeckCountShouldEqualCardCount()
{
var cards = new List<Card> {new Card(), new Card(), new Card()};
int initialCardCount = cards.Count;
var deck = new Deck(cards);
Assert.AreEqual(initialCardCount, deck.Count);
}
[Test]
public void DeckShouldHaveOneLessCardAfterDrawing()
{
var cards = new List<Card> {new Card(), new Card(), new Card()};
int initialCardCount = cards.Count;
var deck = new Deck(cards);
Card card = deck.DrawCard();
Assert.AreEqual(initialCardCount - 1, deck.Count);
}
When we compile this, we get a build error because we have a
little bit of implementation to do. We can assume that our 3 classes exist as
empty shells for now, so the remaining work to fix the compiler errors is to
create a constructor for the Deck class which takes a collection of cards,
create a method called DrawCard, and create a property called Count. When we
first create these we can just have them each throw a NotImplementedExcetion.
Then we will no longer have the compiler errors. This is a good opportunity to
run the tests and confirm that they indeed fail. This means that our code needs
to be written. Having a failing test is kind of like having a task in the
queue.
I am not going to cover running NUnit in this article, there are plenty of
articles and tutorials that demonstrate how to run that application.
|
Making the Tests Pass |
I do not want to spend a lot of time talking about our
implementation which makes the tests pass, because it is not the focus of the
article. For now I will just include a quick solution which causes the tests to
pass. Keep in mind that at each step we should only write just enough to make
the tests pass. We will write more tests in a moment which help us create more
functionality. If we flesh out the Deck class with code that actually works, we
end up with something similar to the following code.
Listing 2: The Simple Implementation
public class Deck
{
private readonly List<Card> _cards;
public Deck(List<Card> cards)
{
_cards = cards;
}
public int Count
{
get { return _cards.Count; }
}
public Card DrawCard()
{
Card drawnCard = _cards[0];
_cards.Remove(drawnCard);
return drawnCard;
}
}
Notice here that there are still plenty of bugs which can
occur with this code. For example if the deck is empty, I am going to get an
ArgumentOutOfRangeException. We need to get some more tests in place and make
them pass.
|
Creating More Tests |
As a general rule, I like to have a test for the common ways
things work as well as the exceptional cases, and I like to create tests which
reflect how the classes are used. We want to have at least a happy case and a
sad case. The happy case is where everything runs as expected, and obviously
the sad case is when it is the reverse. This can mean that we have an exception
thrown or that we handle the bad occurrence. Since they are easy to create, I
will start by handling an exceptional case. We need to now ask ourselves what
should happen if we run out of cards. After coming up with the answer, we
should create a test which states exactly that.
Listing 3: Handling Special Cases Testing
[Test]
public void EmptyDeckShouldReturnNullWhenDrawing()
{
// Create an empty Deck
var deck = new Deck(new List<Card>());
Card card = deck.DrawCard();
Assert.AreEqual(null, card, "A Card was returned from an empty Deck.");
}
The important thing we did here is that we defined what we
expect to happen here. This obviously doesn't happen yet, we get an exception
for now. We want to make sure that we get this expected result. Once we have
this test in place and passing, we know what to expect anywhere that calls this
method. When we execute this method, we get the exception we know we would get,
which is good. The System.ArgumentOutOfRangeException lets us know that the
application worked as we knew it would, because we have not written the code to
make this test pass yet. While we are in the test class, we will add more tests So this article doesn't get too out of hand, we will be creating multiple tests at a time. We will also add a test which makes sure that the Card we draw is no longer in the Deck.
Listing 4: More Testing
[Test]
public void DeckShouldNotContainACardAfterDrawingIt()
{
var deck = new Deck(new List<Card> { new Card(), new Card(), new Card() });
Card card = deck.DrawCard();
Assert.IsNotNull(card);
Assert.IsFalse(deck.ContainsCard(card));
}
This test will also fail, because we haven't created a
method to check if a card is in the Deck. If we add in an empty method that
just throws an Exception, we will have it fail for that reason. Now we need to
add in the code to make these two methods pass.
Listing 5: Implementing Passing Code
public bool ContainsCard(Card card)
{
return _cards.Contains(card);
}
Since we already wrote most of the code, this was all that
was needed to make our tests pass. Normally I write as few tests as possible at a time, so I can better focus on making sure each test is implemented correctly. This helps to make sure I am not diluting my focus when creating the interactions between objects.
|
Download |
|
Conclusion |
In Test Driven Development, people use the phrase,
"Red, Green, Refactor". We say this because we decide what to write
based on the failing test we have. Once we get the test passing, we can go in
and refactor things without worrying that we are breaking this functionality.
Remember that tests are there to maintain your code as well as to give you the
opportunity to decide what interface you want for your code.
For more content, read Brendan
Enrick's Blog.
|
|
|
User Comments
No comments posted yet.
|
|
|