Test Driven Development

Test-Driven development is a rewarding practice, that stands out in eXtreme Programming. All the other practices of XP can usually be compromised when dealing with distributed teams, but the one practice that can and must be followed always - is test-driven development (TDD).

What is Test-Driven Development ?

Test Coverage is a philosophy - that for every bit of logic in the system, there is a test validating that the logic is correctly implemented (Unit Tests), or that the logic is correct at all from an external system perspective (Functional Tests). The HTMLParser project also follows quick releases - and in order to ensure that a new release does not add more bugs, we ensure that -

1. No existing tests break

2. The new code has test-coverage

However, Test-Driven Development (TDD) is not about writing tests for your code - it is writing tests before your code. The reason we do this - is to keep the system as simple as possible, and just write enough code to make the tests pass. After all the architecture and patterns we study, it is dangerous if we cannot control our knowledge and use it indiscriminately. TDD allows tests to drive the coding process, thus helping us produce a really simple system. It is possibly a paradigm shift for many programmers who write tests after the code, or don't write tests at all. But you cannot produce maintainable systems of high quality without discipline. Hence, we really like to enforce discipline on contributions to this project.

I am sold on tests - now what ?

Its time to take a look at the test-framework in place that enables quick and easy testing - as we believe that testing should be painless. If it is too hard, we wouldnt do it. To begin with, as is the case with most Java XP projects of the day, we use JUnit. JUnit allows us to create suites of tests and run them automatically. In case you are new to unit testing, please make sure that you have read and tried out the examples from Test Infected - Programmers Love Writing Tests.

Once you are comfortable with JUnit, you are ready to start writing tests for the parser. We provide you with a utility class - ParserTestCase - with which you can rig up a complex test pretty easily. This class is in the org.htmlparser.tests package in the src.zip.

Writing a testcase with ParserTestCase

ParserTestCase provides you with utility methods to create parser objects quickly and easily for sample html that is being tested. Once you have the parser objects, you have another utility method to parse it and put all the parsed objects into an array of nodes - which is then available to you for assertions.

Lets try writing a simple test - we have a bug report - that the parser throws exception when tags of the type <base ...> are encountered without an href attribute. Base tags can also have target attributes like <base target="_top">, but for some reason, the parser doesen't accept this and throws an exception. Before we start to fix the bug, we write a test case to prove that the bug exists.

Since there already exists a testcase called BaseHrefTagTest, we shall add our test to this class.

   public class BaseHrefTagTest extends ParserTestCase {
     ...
   }

Note that all testcases for the parser extend from ParserTestCase.

We shall call our method - testNotHrefBaseTag() - for it describes the case when the base tag is not a base href, but something else. It is very important to write intention-revealing code. This is the reason we can spend less time on documentation and more time on coding.

Below, we present the code for the method testNotHrefBaseTag():

  public void testNotHrefBaseTag() throws ParserException {
    createParser("<base target=\"_top\">");
    parser.registerScanners();
    parseAndAssertNodeCount(1);
    assertTrue("Should be a base tag but was "+node[0].getClass().getName(),node[0] instanceof HTMLBaseHREFTag);
    BaseHrefTag baseTag = (BaseHrefTag)node[0];
    assertEquals("Base Tag HTML","<base target=\"_top\">",baseTag.toHTML());
  }

In Line 2, we make a call to createParser() - this creates a parser object internally for us, that operates on the data provided in the parameter - the test data. This simulates the situation that the parser encounters a line containing the given test data, that should result in the parser throwing an exception upon parsing - if it is buggy.

In Line 3, we would like to register all the scanners available - with a call to parser.registerScanners(). When writing tests, it is usually a good idea to do this so as to verify that in a practical situation, when all scanners are registered, there are no undesirable side-effects - i.e. even if your scanner works fine individually, it might not when it is in a system of several other scanners.

In Line 4, we ask the parser to pass through this data and collect all the html nodes into an array, and also verify that the number of nodes found were what we expected. This is done with a call to parseAndAssertNodeCount().

After a call to this method - we have with us an array called "node" - which contains all the parsed objects. We can now take control and do our assertions to see what it contains. Line 5 has an asertTrue(), that verifies that the first node object is indeed a base tag, and if not, it provides the info about the object that was found instead.

Once control passes to line 6, we are sure that what we have is a base href tag - and we can proceed to downcast it to BaseHrefTag. Once we have our base tag - we can easily test its contents in Line 7.

An important detail to note here is that - your test method must have a throws clause - declaring that it throws an ParserException. Do not surround your code with a try-catch, unless you are testing that exceptions should be thrown (there are better ways of doing that as well). This is because, JUnit automatically catches any exception thrown and shows the error in the test runner.

The above example is from an actual bug-reproduction and fixing session. After the test failed - the bar went red - we had the proof that the bug existed. This was then fixed, and when the bar showed green, it meant that the bug had been eliminated, destryoed, annhilated, whatever.. Death to the bugs!

ParserTestCase also offers an assertStringEquals() method, that does a character-by-character check of two strings and displays valuable mismatch information that is usually not available when you use assertEquals().

Communicate with testcases

Open-source projects provide a good way of having an expanded testing base. This has proved to be very true in case of the htmlparser project. It is therefore important for us to look at patterns of reporting bugs which enable us to maximize the communication bandwidth, and minmize the involvement time of developers.

As human beings - language often fails us when we need it most. We do get bug reports like, "The parser barfs at the url .... with this exception ...". Such a report assumes that someone has the time to try out the url, find the exception, write a test to reproduce the situation, and then fix it. Time is a precious commodity on open-source projects, and bug reports that are not informative enough are often a strain on the developers supporting the project.

We can remedy this with a high-class bug report of the form - "The parser barfs at the url ... with the exception ... It is a problem with the _ tag , and I wrote a testcase to reproduce this bug. Here is the testcase".

Such bug reports make it a pleasure for any developer to simply take the testcase, run it, find the bug, and fix it. We've often had issues when people reported bugs which we didn't think existed. So we wrote testcases and proved that the code was indeed bug-free (or not).

We encourage users of the parser to get familiar with writing tests for the parser and submit testcases in their bug reports.

--SomikRaha, February 16, 2003 11:55:42 am


Last edited on Sunday, February 23, 2003 5:44:27 pm.