Login | Register   
LinkedIn
Google+
Twitter
RSS Feed
Download our iPhone app
TODAY'S HEADLINES  |   ARTICLE ARCHIVE  |   FORUMS  |   TIP BANK
Browse DevX
Sign up for e-mail newsletters from DevX


advertisement
 

Test Drive Test-Driven Development with Visual Basic 6 : Page 2

Test-driven software development lets you utilize the power of automated test suites without having to use a different language. Experience the productivity gains and fun that can be had using this agile development best practice.


advertisement
The Problem
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 CArrayUtil End Sub Private Sub IFixture_TearDown() Set m_xArrayUtil = Nothing Erase m_lData End Sub Private 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 Function Private 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_lData End Sub Public Sub TestReverseTenElements() m_lData = getTestData(10) m_xArrayUtil.Reverse m_lData checkResult m_lData End 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_lData Catch: 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.



James Abley is a developer with a financial services company in the South of England. He has been interested in agile methods for the last couple of years and likes to play with various technologies in his spare time so that he can spend more time at work focusing on the interesting rather than routine aspects of software development.
Comment and Contribute

 

 

 

 

 


(Maximum characters: 1200). You have 1200 characters left.

 

 

Sitemap