gile development is the new paradigm! (Are you rolling your eyes yet?) Yes, “paradigm” is an overused word but hype aside, agile development does offer some interesting best practices that might turn on its head how you think about developing software. One such practice is test-driven development (TDD), which has gotten a bit of notoriety lately thanks to its association with Extreme Programming.
The basic premise of TDD is to always write an automated test for a piece of functionality first, before you code up the functionality itself. Having an automated test suite ensures that you only ever provide the simplest implementation of the code so that the new test passes. You then write more tests and accordingly alter and extend, or refactor, the application code to get you where you want to be. In this article I will show the rhythm of this approach in development and introduce the one of the available tools for VB6. I’ll walk through a simple example, where the focus is on the approach, rather than design or coding problems.
Proponents of this style of development frequently speak about the rhythm of TDD, which consists of:
- Write a failing test
- Get the test running
- Get the test passing
- Refactor out any duplication
- Rinse and repeat.
|Figure 1. Green Bar of Freedom: The vbUnitFree Test Runner gives a green bar to indicate all tests have passed.|
The tool that I’ll be using is vbUnit, a Visual Basic implementation of the xUnit design. XUnit is the name given to the testing framework design originally created by Kent Beck, the founder of Extreme Programming. JUnit is the most widely used implementation, but it has been ported to more than 30 different languages, so you can safely consider it to be an industry standard. As is customary with xUnit tools, in vbUnit a red bar means that at least one test failed; a green bar means that all tests passed and you can move on to “rhythm” step 4 (see Figure 1).
|What You Need|
| Visual Basic 6
vbUnit3 framework, available free from http://www.vbunit.com
Either the Professional/Evaluation version of the vbUnit3 Test Runner
or the free vbUnitFree Test Runner, available from http://sourceforge.net/projects/vbunitfree/.
The example problem I’ve created for this article is the fairly trivial case of wishing to reverse an array of Longs. The solution is fairly obvious, and you can always look up the algorithm in a suitable text or reference book or try to find one using Google. But here is the reverse method I’m using:
Public Sub Reverse(ByRef lParams() As Long) Dim i As Long Dim tmp As Long For i = 0 To UBound(lParams) / 2 - 1 tmp = lParams(i) lParams(i) = lParams(UBound(lParams) - i) lParams(UBound(lParams) - i) = tmp Next i End Sub
But you’re a test-driven developer, so instead you create a new vbUnit test project and write a test. It uses the standard vbUnit convention of being a Public Sub that is named testXXX, so that it is picked up by the reflection-like abilities of TypelibInfo.dll.
Public Sub TestReverseOneElement() Dim xArrayUtil As New CArrayUtil Dim lArray(0 To 0) As Long lArray(0) = 1 xArrayUtil.Reverse lArray m_xAssert.LongsEqual 1, lArray(0), "Element is not one"End Sub
If you try to run the Test 1 it won’t compile; you get compile error: User-defined type not defined. The next step is to decide upon the simplest change that will get it working. In this instance, it’s to add a Project that contains the CArrayUtil class, add a Reverse method that takes a Long array as an argument, and then add a reference to your new Project to the test Project.
Now it runs and passes. Progress! There doesn’t appear to be any duplication so far, so you can write another test. This time, it’s an array with two elements:
Public Sub TestReverseTwoElements() Dim xArrayUtil As New CArrayUtil Dim lArray(0 To 1) As Long lArray(0) = 1 lArray(1) = 2 xArrayUtil.Reverse lArray m_xAssert.LongsEqual 2, lArray(0), "first element is not 2" m_xAssert.LongsEqual 1, lArray(1), "Second element is not 1" End Sub
This test runs, but fails. There’s a problem with the assertion that checks that the elements have been transposed.
Again, your job is to find the simplest way to get the test running. You create a for loop that uses the upper bound of the array and a temporary variable to iterate through the array, swapping the n and (length – n) elements until it reaches the middle. The tests pass and you don’t appear to have broken anything in the process. So you’re once again at stage four of the process. Is there any duplication?
Yes, but only in the test code. First off, you are using a CArrayUtil object in both test methods. You can refactor this out into the IFixture_Setup and IFixture_TearDown methods, which are called before and after each test, respectively (see sidebar, “The IFixture Interface“).
The second duplicated area?which isn’t immediately apparent, but would be if you were to add another test with an array that contained more elements?is how you are declaring and populating the test arrays. To solve this problem, move the array population code into a private getTestData method and use a module-level m_lData variable rather than a local declaration in each method. Once again, running the tests after each change confirms that you haven’t broken anything.
As a symmetrical refactoring to the array population code, a third area of duplication is in the assertion code after the array has been reversed. To solve this you create the private checkResult method; again, running the tests as often as you can stand in order to confirm checkResult is implemented correctly. Of course, you don’t need to factor out this common code in all of your tests and it can make it difficult to see what is going on if you have a lot of setup code to run a single test. But it is the natural choice in this instance because it requires only a couple of variables being moved up into the Fixture, and thus it nicely illustrates use of the IFixture interface.
The refactored test code below shows the test setup and teardown code, along with the array population and checking code:
Private Sub IFixture_Setup(assert As IAssert) Set m_xAssert = assert Set m_xArrayUtil = New CArrayUtilEnd SubPrivate Sub IFixture_TearDown() Set m_xArrayUtil = Nothing Erase m_lDataEnd SubPrivate Function getTestData(ByVal lElements As Long) As Long() Dim i As Long Dim ret() As Long ReDim ret(0 To lElements - 1) As Long For i = 0 To lElements - 1 ret(i) = 2 ^ i Next i getTestData = ret End FunctionPrivate Sub checkResult(ByRef lArray() As Long) Dim i As Long For i = LBound(lArray) To UBound(lArray) m_xAssert.LongsEqual 2 ^ (UBound(lArray) - i), lArray(i), _ "Element index " & CStr(i) & " is not " & CStr(lArray(i)) Next i End Sub
The next step is to add a couple of other tests to check that the 9 and 10 small element arrays being used do not have any hidden edge cases and are consecutive, thereby having no off-by-one errors:
Public Sub TestReverseNineElements() m_lData = getTestData(9) m_xArrayUtil.Reverse m_lData checkResult m_lDataEnd SubPublic Sub TestReverseTenElements() m_lData = getTestData(10) m_xArrayUtil.Reverse m_lData checkResult m_lDataEnd Sub
If the test still passes you can now look at how an empty array is handled. This would typically be defined in your requirements, or you would have to make a choice as to whether the responsibility for array handling lies with the calling client or the implementation. In my code I’ve assumed that the client will handle this task:
Public Sub TestEmptyArray()On Error GoTo Catch m_xArrayUtil.Reverse m_lDataCatch: m_xAssert.LongsEqual ArrayUtilErrors.ArrayNotDimensioned, _ Err.Number, "Testing for error thrown when array is empty"End Sub
The code won’t compile, initially because you haven’t yet defined the error number. Add the code to handle that condition, though, and it compiles and fails. Add the isDimensioned check to the Reverse implementation and … success! That green bar is getting addictive.
It’s worthwhile to note that none of the tests thus far are tightly coupled to other tests. This is ideal because if a test fails, you don’t want 20 other tests to fail as well. Far better to just to investigate the single case to see what went wrong.
At this point, the implementation is reasonably complete. Depending on the requirements, you may add other tests that use an array that isn’t zero-based, change the type to allow other types of arrays to be reversed, etc. Admittedly, this a quite a heavy-weight approach for such a simple problem, but it’s a very beneficial way to go about development for more involved projects; having automated unit tests for any application code that you create is not a bad thing, and I’m sure you’ll agree.
In my experience, this method of developing has meant a greater adherence to the Command-Query separation principle I often have more methods on a public interface that are only used in the tests. But being able to write a batch file or something similar to run a large suite of tests against my application code is definitely a worthwhile trade-off for a larger public interface. I can schedule a batch file to run my tests for a given area of the system, thus giving early feedback when a change might have rippled and broken another part of the system. An additional benefit is improving the design by having a larger number of smaller classes and thus allowing you to favour composition when extending the desired functionality.