Step 4 - The InsertStringBuffer Implementation
In the last step we have defined the interface and the behavior of the StringBuffer class, and we have provided a trivial implementation that shows that the basic design of the interface is working. If performance would be of no concern, we would be done now. However, performance is a concern in this particular case, so let's add another StringBuffer implementation that should be a little faster.
Implementation 2: InsertStringBuffer
The algorithm that we have used so far used incremental concatenation. For every litte fragment to be added to a string, the memory holding the entire string was reallocated. Maybe we can design an algorithm that reduces the number of memory reallocations. This is made possible by the Mid$ function of VB. Although this is not made clear in the VB reference, you can actually assign to the Mid$ function, so that it can be used to insert characters into a string without reallocating it. This is exactly what we need. Hence our algorithm will allocate a reasonably large string to start with and just insert the incoming fragments into it. If the buffer runs out of space, it's size will be doubled.
Therefore, if we wanted to grow a string to the size of 65k in increments of one character, the ConcatStringBuffer would require roughly 65000 reallocations of the entire StringBuffer. In contrast, the InsertStringBuffer algorithm will only require about 16 reallocations for the same task. Sounds better, doesn't it?
Let's see if we can get this to work:
Create a new class, call it "InsertStringBuffer" and add the following code to it:
'InsertStringBuffer.cls Option Explicit Implements IStringBuffer Private m_buffer As String Private m_used As Long Private m_allocated As Long Private Sub Class_Initialize() Const InitialSize As Long = 32 m_buffer = String$(InitialSize, " ") m_allocated = InitialSize m_used = 0 End Sub 'double the allocated buffer Private Sub Grow() m_buffer = String$(2 * Len(m_buffer), " ") m_allocated = Len(m_buffer) End Sub Private Sub IStringBuffer_Append(str As String) Dim length As Long Dim requiredSize As Long 'make sure the buffer is large enough and grow if necessary length = Len(str) requiredSize = m_used + length If requiredSize > m_allocated then Grow End If 'insert new string into the buffer Mid$(m_buffer, m_used) = str m_used = m_used + length End Sub Private Property Get IStringBuffer_Length() As Long IStringBuffer_Length = m_used End Property Private Property Get IStringBuffer_Result() As String IStringBuffer_Result = Left$(m_buffer, m_used) End Property
To make sure that this is in fact working, add the following method to the TestStringBuffer class:
'TestStringBuffer.TestInsertStringBuffer Public Sub TestInsertStringBuffer() Set m_stringBuffer = New InsertStringBuffer FillBufferAndCheckResult End Sub
Now run it in the vbUnit TestRunner. You should get the following result:
ERRORS: 1
TestStringBuffer.TestInsertStringBuffer : Invalid procedure call or argument : RunTest
Oops! It seems that there is a problem somewhere in the code. The vbUnit framework has caught an error in the "TestInsertStringBuffer" method of our fixture. Let's see if we can pinpoint the location of the error.
Finding the error
Put a breakpoint at the first line of the "TestInsertStringBuffer" method and run the test again (make sure it is running in debug mode).
When the debugger stops at the breakpoint, right-click in the code window and change the Error Trapping mode to "Break on All Errors":
Figure 9: Changing the error mode right before the problem
Now continue the debug session with "Run / Continue" (or by pressing F5). You should now get a messagebox from VB indicating a run-time error:
Figure 10: Letting VB find the error
Click on "Debug" to jump to the part of the code where the error occured. This should take you to the "IStringBuffer_Append" method of the InsertStringBuffer class:
'InsertStringBuffer.Append Private Sub IStringBuffer_Append(str As String) Dim length As Long Dim requiredSize As Long 'make sure the buffer is large enough and grow if necessary length = Len(str) requiredSize = m_used + length Do While requiredSize > m_allocated Grow Loop 'insert new string into the buffer Mid$(m_buffer, m_used) = str m_used = m_used + length End Sub
It seems that the line Mid$(m_buffer, m_used) = str contains the problem. The characters in a VB string are counted starting from 1, not from 0, but the variable m_used starts with 0. Therefore, change this line into
Mid$(m_buffer, m_used + 1) = str
Now run it again. This time you should get:
OK (2 Tests, 4 Assertions)
This example has shown how you can run unit tests in the debugger and use vbUnit to interactively test and debug your code. Before you continue, remember to change the Error Trapping mode back to "Break on Unhandled Errors". Otherwise you will get a run-time error messagebox at the next failed assertion.
Summary
This section has also shown how vbUnit can be used together with the VB debugger to find run-time errors in the code.
- The error trapping mode can be changed on the fly by right-clicking into a code window and then using the context menu.
- Normally, the mode should be set to "Break on Unhandled Errors", because otherwise the debugger will break at every failed assertion.
- If you keep the number of failures at zero, you don't need to set a breakpoint before an error. Just change the error trapping mode before starting the TestRunner. The breakpoint is only needed if some failures occur before the error, because then you cannot change the error trapping mode in advance (or execution will always stop at the first failure).