In a hot discussion thread in msforums.ph about the value of unit testing and adopting test-driven development (TDD), a suggestion was made to illustrate how TDD is used in creating a simple application, specifically Keith's October code challenge. In response to that, I've decided to write a series of articles showing exactly how this can be done. I guess there's no better way to show the value of TDD than by showing how it's done!
I'll be using Visual Studio 2008 for this tutorial, as well as the excellent TestDriven.NET add-in and the Moq mocking framework. I will also be using the built-in MSTest framework, for a change, just to see if I'll like it 
If you are not yet familiar with the test-driven or test-first approach to software development, you can take a look at my old blog entry with presentation slides and my two sample scenarios on using mock objects.
So just to review Keith's challenge:
- The user should be able to search for books base on a selected Testament (Old and New).
- The user should be able to see the contents of each Book
- The user should be able to search the contents of the Bible based on different search criterias like "Luke", "Genesis 1", "John 3:16", "love", "Abraham" and the application should be able to return the matching results.
- The user should be able to jump from one book to another.
- The user should be able to jump from one chapter to another.
OK so these are the system requirements. I downloaded the zipped MS Access database file and extracted it, ready for use. We will proceed by creating a solution containing two class library projects -- BibleCodeTests, containing our tests, and BibleCode, containing our actual code. So for the first test, let's imagine making a request to our database -- a service -- to retrieve a particular testament by name, and receiving the desired Testament object. Let's do just that:
-
using BibleCode;
-
using Microsoft.VisualStudio.TestTools.UnitTesting;
-
using Moq;
-
-
namespace BibleCodeTests {
-
[TestClass]
-
public class EntitiesTests {
-
[TestMethod]
-
public void GetOldTestament() {
-
var mock =
new Mock<IDataService>
();
-
mock.Expect(srv => srv.GetTestamentByName("Old Testament"))
-
.Returns(GetOldTestamentObject())
-
.AtMostOnce();
-
IDataService service = mock.Object;
-
Testament oldTestament = service.GetTestamentByName("Old Testament");
-
Assert.AreEqual(1, oldTestament.Id);
-
Assert.AreEqual("Old Testament", oldTestament.Name);
-
mock.VerifyAll();
-
}
-
-
private Testament GetOldTestamentObject() {
-
return new Testament
{ Id =
0, Name =
"Old Testament" };
-
}
-
}
-
}
Wow that's quite a mouthful. Here we make use of a mock object to simulate our database, and we will be creating an IDataService interface for that. For our unit tests, we will just use a mock implementation of IDataService that's automatically created by the Moq framework, but in our actual code (later in this tutorial series) of course we will use the one that actually talks to the MS Access database. Once we create the mock object, we set up the expectation that our client code should call the data service's GetTestamentByName method/operation, passing the name of the testament we want to receive as input. Then we should get a Testament object as output, with its Id property set to 1 and its name properly set.
Of course this test wouldn't pass yet; it won't even compile properly. Let's put in the minimal stuff necessary to get this test to pass. So for IDataService.cs (in the BibleCode project):
-
namespace BibleCode {
-
public interface IDataService {
-
Testament GetTestamentByName(string name);
-
}
-
}
And in our Testament.cs (also in the BibleCode project):
-
namespace BibleCode {
-
public class Testament {
-
public int Id { get; set; }
-
public string Name { get; set; }
-
}
-
}
This should compile now, and if we run it we'll see that our single test has failed. The failure points to line #16 in the test code, and we realise why: the test was expecting an output Id of 1 but our GetOldTestamentObject() method returns an object with an output Id of 0. This is a problem with our test code so we edit the test code, changing the Id property in the returned Testament object to 1. When you re-run the test, this time it will pass. Hooray!
You might be asking yourself what the purpose of this test actually is. After all, we are not yet constructing the actual class that connects to the database; we have just specified an interface for it, IDataService, and we simply used a mock object constructed to conform to that interface. And the thing that we're testing -- using Asserts -- simply got its values from a private method that we created within the test class itself! Well, the purpose of creating this test is to drive our design. We're not using it to test anything at this point; we're just specifying through our test class and methods how we want our classes to look like and work like. At this point it may sound like a pointless exercise to you, but later on you should see how this approach actually helps in development.
Continuing further, let's create our second test, this time for retrieving books. Let us again pretend we will create a method named GetBookByName for our data service where we pass the name of the book we want to retrieve. In return, it will give us a Book object. In our EntitiesTests.cs file, add the following method inside the EntitiesTests class:
-
[TestMethod]
-
public void GetBookOfGenesis() {
-
var mock =
new Mock<IDataService>
();
-
mock.Expect(srv => srv.GetBookByName("Genesis"))
-
.Returns(GetGenesisObject())
-
.AtMostOnce();
-
IDataService service = mock.Object;
-
Book genesis = service.GetBookByName("Genesis");
-
Assert.AreEqual(1, genesis.Id);
-
Assert.AreEqual("Genesis", genesis.Name);
-
Assert.AreEqual(50, genesis.Chapters.Length);
-
mock.VerifyAll();
-
}
-
-
private Book GetGenesisObject() {
-
-
{
-
Id = 1,
-
Name = "Genesis",
-
Chapters =
new Chapter
[50]
-
};
-
}
It's more of the same from the previous test, but take a look at how we're using this test to drive our design. We call a GetBookByName() method in the data service, passing the string "Genesis" to it. In return, we get a Book object and it's got an Id property, a Name property and a Chapters array property containing all chapters in that book.
In order to get this to compile, we will need to create a Book class, in Book.cs:
-
namespace BibleCode {
-
public class Book {
-
public int Id { get; set; }
-
public string Name { get; set; }
-
public Chapter[] Chapters { get; set; }
-
}
-
}
And it also requires a Chapter class, so we need one for Chapter.cs:
-
namespace BibleCode {
-
public class Chapter {
-
}
-
}
Note how minimal our Chapter class is...we will develop it as we go and define more tests for our code later. Finally for IDataService.cs:
-
namespace BibleCode {
-
public interface IDataService {
-
Testament GetTestamentByName(string name);
-
Book GetBookByName(string name);
-
}
-
}
So now you should be able to compile and run the code with the tests passing. Let's get back into creating another test, this time for retrieving a chapter given a book and the desired chapter number:
-
[TestMethod]
-
public void GetChapter1OfGenesis() {
-
var mock = new Mock<IDataService>();
-
Book genesis = GetGenesisObject();
-
mock.Expect(srv => srv.GetChapter(genesis, 1))
-
.Returns(GetGenesisChapter1())
-
.AtMostOnce();
-
IDataService service = mock.Object;
-
Chapter chap = service.GetChapter(genesis, 1);
-
Assert.AreEqual(1, chap.Id);
-
Assert.AreEqual(31, chap.Verses.Length);
-
mock.VerifyAll();
-
}
-
-
private Chapter GetGenesisChapter1() {
-
-
{
-
Id = 1,
-
-
};
-
}
Again, this code will not compile for obvious reasons so let's do what it takes to pass this test. In Chapter.cs:
-
namespace BibleCode {
-
public class Chapter {
-
public int Id { get; set; }
-
public string[] Verses { get; set; }
-
}
-
}
And another change in IDataService.cs:
-
namespace BibleCode {
-
public interface IDataService {
-
Testament GetTestamentByName(string name);
-
Book GetBookByName(string name);
-
Chapter GetChapter(Book book, int chapterId);
-
}
-
}
OK, so can you see the pattern of what we're doing here? One by one we're fashioning methods for our data service interface (which will be the one taken by our actual database class, later in this tutorial) and we're specifying how we want to call it and what we expect out of it. This is called intentional programming, and we are specifying our intentions -- in code. Let's add another test, this time we want to say that when we retrieve a Testament object, we want it to have all its Book objects inside already. So here's the test code:
-
[TestMethod]
-
public void GetOldTestamentLoadsAllBooks() {
-
var mock = new Mock<IDataService>();
-
mock.Expect(srv => srv.GetTestamentByName("Old Testament"))
-
.Returns(GetOldTestamentObject())
-
.AtMostOnce();
-
IDataService service = mock.Object;
-
Testament oldTestament = service.GetTestamentByName("Old Testament");
-
Assert.AreEqual(1, oldTestament.Id);
-
Assert.AreEqual("Old Testament", oldTestament.Name);
-
Assert.AreEqual(39, oldTestament.Books.Length);
-
Assert.AreEqual("Genesis", oldTestament.Books[0].Name);
-
mock.VerifyAll();
-
}
This is almost the same as our first test, but notice that we are now expecting that there is a Books property with a Length of 39, representing the 39 books of the Old Testament. We also ensure that the first book in the entry is the book of Genesis. In order to pass this test, we need to modify the Testament class in Testament.cs:
-
namespace BibleCode {
-
public class Testament {
-
public int Id { get; set; }
-
public string Name { get; set; }
-
public Book[] Books { get; set; }
-
}
-
}
Running the test still results in failure, because we haven't modified our GetOldTestamentObject() private method in the test class yet! Let's change it to:
-
private Testament GetOldTestamentObject() {
-
Testament oldTestament = new Testament
-
{
-
Id = 1,
-
Name = "Old Testament",
-
-
};
-
oldTestament.Books[0] = GetGenesisObject();
-
return oldTestament;
-
}
The tests should now pass. Note what we did here: in our GetOldTestamentObject() method we did create an array of Books, with 39 elements, but we only populated the first element of the array; the rest still contain null. Well, we did only what was necessary to pass the tests, that's why we didn't bother with the other 38 books. Besides, this is just a mock object! We're only using it to specify our intentions and to drive our design. Keep that in mind as we go along. Let's create another test for retrieving all Testaments (there are only two, anyway). We will most likely need the IDataService method we will derive here whenever our application starts.
-
[TestMethod]
-
public void GetAllTestaments() {
-
var mock = new Mock<IDataService>();
-
mock.Expect(srv => srv.GetAllTestaments())
-
.Returns(GetTestamentsArray())
-
.AtMostOnce();
-
IDataService service = mock.Object;
-
Testament[] testaments = service.GetAllTestaments();
-
Assert.AreEqual(2, testaments.Length);
-
Assert.AreEqual("Old Testament", testaments[0].Name);
-
Assert.AreEqual("New Testament", testaments[1].Name);
-
mock.VerifyAll();
-
}
-
-
private Testament[] GetTestamentsArray() {
-
return new Testament[] { GetOldTestamentObject(), GetNewTestamentObject() };
-
}
We will need a GetNewTestamentObject() method in our test class as well:
-
private Testament GetNewTestamentObject() {
-
return new Testament { Id = 2, Name = "New Testament" };
-
}
We need to add the new GetAllTestaments() method to our IDataService.cs:
-
namespace BibleCode {
-
public interface IDataService {
-
Testament[] GetAllTestaments();
-
Testament GetTestamentByName(string name);
-
Book GetBookByName(string name);
-
Chapter GetChapter(Book book, int chapterId);
-
}
-
}
Verify that the tests indeed pass, then move on to our next test: we need to be able to retrieve matching Bible verses given some text. Here is how our test looks like:
-
[TestMethod]
-
public void FindVersesMatching() {
-
var mock = new Mock<IDataService>();
-
mock.Expect(srv => srv.FindVersesMatching("some text"))
-
.Returns(GetVerseResultArray())
-
.AtMostOnce();
-
IDataService service = mock.Object;
-
VerseMatch[] verses = service.FindVersesMatching("some text");
-
Assert.AreEqual(2, verses.Length);
-
Assert.AreEqual("John", verses[0].BookName);
-
Assert.AreEqual(3, verses[0].Chapter);
-
Assert.AreEqual(16, verses[0].Verse);
-
Assert.AreEqual("1 John", verses[1].BookName);
-
Assert.AreEqual(3, verses[1].Chapter);
-
Assert.AreEqual(16, verses[1].Verse);
-
mock.VerifyAll();
-
}
-
-
private VerseMatch[] GetVerseResultArray() {
-
return new VerseMatch[] {
-
new VerseMatch { BookName = "John", Chapter = 3, Verse = 16 },
-
new VerseMatch { BookName = "1 John", Chapter = 3, Verse = 16 }
-
};
-
}
This would require a VerseMatch class, containing information on matching verses for the search:
-
namespace BibleCode {
-
public class VerseMatch {
-
public string BookName { get; set; }
-
public int Chapter { get; set; }
-
public int Verse { get; set; }
-
}
-
}
And a new method in IDataService.cs:
-
namespace BibleCode {
-
public interface IDataService {
-
Testament[] GetAllTestaments();
-
Testament GetTestamentByName(string name);
-
Book GetBookByName(string name);
-
Chapter GetChapter(Book book, int chapterId);
-
VerseMatch[] FindVersesMatching(string text);
-
}
-
}
All of our tests should be passing now. Let's take stock of what we have accomplished so far: we were able to create our data service interface, entity classes and a test class that demonstrates how our eventual application can interact with the actual data service implementation (that will connect to the MS Access database) to get what it needs. Nothing much
but it's a good start. For our Part 2, we will show how we can develop the actual database access code and we will have integration tests to back it up. Stay tuned!
Posted
11-24-2008 8:37 AM
by
cruizer