Slow Start - Testaments and Books

Here is my answer to Keith's Bible App challenge. I know the deadline has already passed but I want to do it the TDD way, so here it is.

Again, the requirements.

  1. The user should be able to search for books based on a selected Testament (Old and New).
  2. The user should be able to see the contents of each Book
  3. 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.
  4. The user should be able to jump from one book to another.
  5. The user should be able to jump from one chapter to another.

So, five requirements, one rule: you have to use the database provided and you are not allowed to use any 3rd party libraries.

How do I approach this?

There's the direct form of attack. I can paint the screen, then generate the datasets and datatables from the database (with permissions from the wizards of vs.net, of course), and then use my lucky One Ring of Power to bind them all together. Nothing wrong with that really. A simple app like this can be written that way.

If this is how you would do it and you are not interested in another way, then press Ctrl-F4 or move on to the next item in your blog/feed/news reader. I wouldn't discuss that here.

What I'm presenting here is the alternative way of attacking this project - one where every piece of non-trivial code is driven and covered by tests - either unit test or integration tests. I'm sure someone would argue that for a simple application such as this, the technique that I'll be using is just plain overkill. I would agree with you. But that's not the point of this article. This is about demonstrating how to write tests for your code.

OK? Good. Let's start.

I've already decided on just copying the look of CryptoKnight's application - treeview on the left hand side, book contents on the right hand side and then a search box on top. I already know what the application does - allow the user to view and navigate through the bible, perform searches, etc. I can think of a couple of ways of implementing this.

  • Option 1 - on launch, the application loads all the data from the mdb file and into memory. This is not unlike how CryptoKnight did it - except that I won't be using datasets but actual objects. The advantage here is that I'll be working with objects for the majority of the application. I can represent the book concepts clearly and without translation - testaments have books, books have chapters and chapters have verses. I can just navigate through the graphs of objects to get from one place to another. Drawbacks: the app should load a little slower because it is loading the data from the database during launch. It should also have a higher memory footprint. I also need to write my own searching algorithm. That shouldn't be that hard though. I should be able to use the String methods to perform the searching and the I'll just wire up the objects themselves to perform the searching for me. It'll should also present a few interesting concepts. For example, I can ask each Book to search their own Chapters and all those can be done on their own separate threads! Not sure if that is needed here, but it's a possibility.

  • Option 2 - only load the root nodes (the testaments) on launch and then load the rest on demand. This should give me a faster launch time. After that, I have two choices - either I only retain the viewable contents in memory, or lazy load the contents as they are being requested. Either one is not a simple as working with the whole set of objects or data in memory though, so that's one of the disadvantages of this approach.

CryptoKnight's version is written using DataSets and all the data is loaded into during launch. So it has already been proven that whole bible can be loaded into memory. Searching is also done in memory - using the dataviews. That is essentially the same as the first option.

Cruizer took the second route.

I think for now I'll do the first scenario. During launch, I'll be loading all the tables and into an object graph rooted at the Testament level. The testaments, books and chapter are in the treeview. Contents will be on the contents pane on the right hand side. Searches are done in-memory.

Here are the initial list of tests based on the requirements.

  • a testament can hold many books
  • a book can hold many chapters
  • a chapter can hold many verses
  • a verse is searchable
  • a chapter is searchable
  • a book is searchable
  • a testament is searchable
  • load object graph from database

The last item is a 'technical requirement'. We can delay the implementation for that as long as we can and we will still have a running application. We can just create the testaments, books, chapters, verses in code. In fact, that is not even interesting - I have written a lot of DALs in my lifetime and that piece is just another DAL. I'd rather start with the real interesting parts of the application -- searching and displaying the book.

The first test - let's check if the tools are properly hooked up.

    using NUnit.Framework;
    
    namespace BibleApp.UnitTests
    {
        [TestFixture]
        public class CoreTests
        {
            [Test]
            public void TestShouldFail()
            {
                Assert.IsTrue(false);
            }
    
            [Test]
            public void TestShouldPass()
            {
                Assert.IsTrue(true);
            }
        }
    }

I run the test. The first test failed and the second test passed. That means the tools are configured properly. I can now write my first real test.

I'll start with the test "a testament can hold many books". I think this can be easily done in one shot but I'll go slow for now. I'll start with a fresh testament.

    using NUnit.Framework;
    
    namespace BibleApp.UnitTests
    {
        [TestFixture]
        public class CoreTests
        {
            [Test]
            public void NewTestamentHasNoBooks()
            {
                Testament t = new Testament("Old Testament");
                Assert.AreEqual(0, t.BookCount);
            }
        }
    }

It doesn't compile because Testament doesn't exist. I'll let Resharper generate the class and properties using a flurry of F12 (Goto next error) and Alt-Enter (QuickFix).

    internal class Testament
    {
        public Testament(string name)
        {
            throw new NotImplementedException();
        }

        public int BookCount
        {
            get { throw new NotImplementedException(); }
        }
    }

Now it compiles but it when I run the test - it fails. Not surprising since I'm throwing NotImplementedExceptions. I'll fix that

    internal class Testament
    {
        public Testament(string name)
        {
        }

        public int BookCount
        {
            get { return 0; }
        }
    }

Now the test passes. But it doesn't do anything - it just returns a fake value. Let's ignore that bit for now and add another test:

    [Test]
    public void TestamentCanHoldOneBook()
    {
        Testament t = new Testament("Old Testament");
        t.AddBook(new Book("Genesis"));
        Assert.AreEqual(1, t.BookCount);
    }

Then create the new class and method.

    internal class Book
    {
        public Book(string name)
        {
        }
    }

    internal class Testament
    {
        public Testament(string name)
        {
        }

        public int BookCount
        {
            get { return 0; }
        }

        public void AddBook(Book book)
        {
        }
    }

It compiles but the test fails. I can fake it again - or not. I will have to implement this properly sooner or later, so why not do it now. I'll use a generic list to hold the book and then simply return the list count as the book count.

    internal class Testament
    {
        private List<Book> _books;
        public Testament(string name)
        {
            _books = new List<Book>();
        }

        public int BookCount
        {
            get { return _books.Count; }
        }

        public void AddBook(Book book)
        {
            _books.Add(book);
        }
    }

There you have it - a unidirectional association of Testaments to Books. I ran both tests and the both pass. I think the test "a testament can hold many books" will now work. Let's see.

    [Test]
    public void TestamentCanHoldManyBooks()
    {
        Testament t = new Testament("Old Testament");
        t.AddBook(new Book("One"));
        t.AddBook(new Book("Two"));
        t.AddBook(new Book("Three"));
        Assert.AreEqual(3, t.BookCount);
    }

And it does - all three tests pass.

There you go - a Testament that can hold many books. Let's scratch off one test.

  • a testament can hold many books
  • a book can hold many chapters
  • a chapter can hold many verses
  • a verse is searchable
  • a chapter is searchable
  • a book is searchable
  • a testament is searchable
  • load object graph from database

You might have notice that the Book and Testament classes are both internal. That is because resharper did not generate separate files for them. I transferred them to their own files, made them public, re-ran the tests, they all pass, and I'm done for now.

Next up, I'm going to show testament objects into the UI and give you a taste on how to write unit tests against the user interface.

Published 11-26-2008 10:32 AM by jop
Filed under: ,

Comments

# re: Slow Start - Testaments and Books

ang bitin naman!!! he he...go go go master jop...

Thursday, November 27, 2008 5:08 AM by cruizer

# re: Slow Start - Testaments and Books

oo nga, nabitin ako! hehehe! keep it coming jop! :)

Thursday, November 27, 2008 10:24 AM by keithrull

# re: Slow Start - Testaments and Books

Wow! Very professional ang approach!

Nahiya ako bigla sa sarili ko. Why wasn't I able to think of Testaments as objects? Galing, elibs ako! (applause)

Friday, November 28, 2008 9:40 PM by lamia

# re: Slow Start - Testaments and Books

I am reminded of one article in Uncle Bob (Robert Martin)'s "The Craftsman" series, in which he said that it takes an abstraction leap to think in terms of objects and not primitive data types :)

Friday, December 05, 2008 4:11 AM by cruizer