Step 3 - Designing the StringBuffer class
In the previous section we have used vbUnit to run some pieces of code to compare them with each other. Note that this wasn't an actual unit test yet, but it shows nicely how vbUnit supports experimenting and prototyping. You can focus on the actual code to be worked on and don't have to waste time writing an interface for it.
Developing with Unit Tests
But now let's go through an actual example of developing with unit tests. At this point I would like to summarize some of the advantages of using a development method that is driven by unit tests:
- Interfaces are contracts : unit tests define and verify them. That's why it is often good to first write the interface, then the unit tests, and then the code. When all tests pass, you are done.
Writing tests first is more motivating: Normally, you write a piece
of code first, then test it, and it breaks. So you continually punish
yourself with failure. Also, you never really know if you are done or not.
If you write a test first, and then work on a piece of code until the test passes, you continually reward yourself with success. Each test defines a small, achievable goal, along with a clear indication whether the goal has been met.
- Further, at every point in the development cycle, you know that all existing fragments of code are already working, since you have tests for them. This defines the difference between hoping that everything will work eventually, and knowing that it already does.
- Unit tests provide an unambiguous specification of what the code should do, most other forms of documentation do not.
- This specification also happens to be working sample code that illustrates the usage of your components.
- Unit tests provide a testbed or a sandbox environment for the components; this facilitates shorter design cycles, since new code can be tested immediately and in isolation, even if dependent components are not yet available.
- High testability enforces a much cleaner design, especially when testing comes first.
- Unit tests provide confidence to make large changes to the code, this allows frequent refactoring (cleaning up the design). They also make it less risky to let new people work on the code, since they will immediately know if they break something.
- They allow full-time testers to focus on difficult high level integration, rather that hunting puny low-level bugs that should never have gotten beyond the developer, and thus decrease load on the whole development team.
- They expose bugs: a newfound bug indicates incomplete unit test coverage. Hence, a test is written to expose the bug. Then the bug is fixed, and the test will indicate when it has been fixed.
Design: Operations for the StringBuffer class
Now back to our project. After we have recognized that iterative string concatenation in VB isn't up to the job, let's design a new class that can handle this more elegantly. I will call this class "StringBuffer", and it should support the following operations:
- represent a String that is grown iteratively
- have an Append( str As String ) method that can be called successively to grow the represented String in small increments
- have a Result() property that returns the represented String
- have a Length() property that returns the length of the represented String
Let's put this interface in code. Add a new class to the tutorial project and call it "IStringBuffer". Then add the following code to it:
'IStringBuffer.cls Option Explicit Public Sub Append(str As String) End Sub Public Property Get Result() As String End Property Public Property Get Length() As Long End Property
At this point we are just defining an interface. Now we will write some unit tests to define what this interface is supposed to do, i.e. what it means for a class to be a StringBuffer. The unit tests should provide an unambigous description of what this class should do, and when all the tests pass, we can be sure that the task of implementing the class is done.
Test: Unit Tests for the StringBuffer design
Add a new "vbUnit TestFixture" class and call it "TestStringBuffer". Make sure to add the line suite.AddFixture New TestStringBuffer to your TestSuite. Add this point, you can also comment out the previous line for adding the Step2 fixture to the suite, since we won't need it now.
Copy the following code into into the Fixture class:
'TestStringBuffer.cls Option Explicit Implements IFixture Private m_assert As IAssert Private m_stringBuffer As IStringBuffer Private Sub IFixture_Setup(assert As IAssert) Set m_assert = assert End Sub Private Sub IFixture_TearDown() End Sub 'fill the buffer and check the result Private Sub FillBufferAndCheckResult() Dim l As Long Dim expectedLength As Long expectedLength = 0 For l = 65 To 90 '"A" to "Z" m_stringBuffer.Append Chr$(l) expectedLength = expectedLength + 1 Next l m_assert.LongsEqual expectedLength, m_stringBuffer.length, _ "checkLength" m_assert.StringsEqual "ABCDEFGHIJKLMNOPQRSTUVWXYZ", _ m_stringBuffer.Result, "checkResult" End Sub
So far, this fixture has a method that fills a StringBuffer with a particular
sequence of characters, and then checks that the result is what it should be.
Filling the buffer is accomplished by repeated calls to m_stringBuffer.Append.
In this case, we are filling the buffer with the letters of the alphabet.
Later, when the buffer is full, we assert certain conditions by calling methods of m_assert. The first assertion,
m_assert.LongsEqual expectedLength, m_stringBuffer.length, "checkLength"
checks that the filled buffer has the correct length. This is obviously a first criterion for judging the success of this operation. The second assertion,
m_assert.StringsEqual "ABCDEFGHIJKLMNOPQRSTUVWXYZ", m_stringBuffer.Result, "checkResult"
compares the actual content of the buffer with the expected result.
The comparison methods of m_assert always have the form
m_assert.TypesEqual expected, actual, message
where Type can be Long, Double,
String, or Variant. For comparing two values, it is better to use these methods
instead of m_assert.Verify, since they will give much more informative failure
descriptions in the results window.
Now that we have a design and some tests for it, we can start with the implementation. Any class that wants to be a StringBuffer must implement the IStringBuffer interface and pass the unit tests for this interface. Separating the interface from the implementation like that will allow us to have several different StringBuffer implementations with different algorithms and compare them with each other. First, we will implement the algorithm that we already know: the one from Step2 of this tutorial. It will simply be a wrapper for str = str & "some text".
Add a new class called "ConcatStringBuffer" with the following code:
'ConcatStringBuffer.cls Option Explicit Implements IStringBuffer Private Sub IStringBuffer_Append(str As String) End Sub Private Property Get IStringBuffer_Length() As Long End Property Private Property Get IStringBuffer_Result() As String End Property
At this point, we still haven't coded the actual implementation. Let's try to get it to compile first and to run the unit test on their own to see them fail. Add the following method to the TestStringBuffer class:
'TestStringBuffer.TestConcatStringBuffer Public Sub TestConcatenatingStringBuffer() Set m_stringBuffer = New ConcatStringBuffer FillBufferAndCheckResult End Sub
Note that the method "TestConcatStringBuffer" is the first TestMethod of this fixture, since it is public and its name starts with "Test". Now you can run the new test for the first time. Save your work and press the "Run" button of the vbUnit window. You should get the following result:
TestStringBuffer.TestConcatenatingStringBuffer : expected '26' but was '0' : checkLength
Click on the result to jump to the corresponding line in the code. The assertion m_assert.LongsEqual expectedLength, m_stringBuffer.Length, "checkLength" failed, as expected, since at this point the ConcatStringBuffer class has no implementation yet. However, the fact that the test gets to this point means that the our test project so far compiles, and that it doesn't crash. Rather, it fails in a nice and expected manner. Now we can add the implementation for the ConcatStringBuffer. Change the code of this class to the following:
'ConcatStringBuffer.cls Option Explicit Implements IStringBuffer Private m_buffer As String Private Sub IStringBuffer_Append(str As String) m_buffer = m_buffer & str End Sub Private Property Get IStringBuffer_Length() As Long IStringBuffer_Length = Len(m_buffer) End Property Private Property Get IStringBuffer_Result() As String IStringBuffer_Result = m_buffer End Property
This is a straightforward implementation of the "incremental concatenation" algorithm discussed in Step2. Let's check that it works correctly, so run the Test again. This time you should get:
OK (1 Test, 2 Assertions)
There was exactly one test (TestConcatStringBuffer), and it made 2 assertions
(the two lines at the end of FillBufferAndCheckResult). The fact that both of
them succeeded suggests that our implementation of ConcatStringBuffer does all
we expect from a StringBuffer object.
However, one of the rules of unit testing is: Never trust a test before you have seen it fail. Who knows if the test is really checking what we think it does?
So let's make it fail. Change the last line of FillBufferAndCheckResult into:
m_assert.StringsEqual "ABCDEFGHIJKLMNOPQRSTUVWXYZx", m_stringBuffer.result, "checkResult"
Now run it again. You should get the following result:
Figure 7: See the test fail at least once
By modifying the expected string, we caused the TestRunner to throw an assertion failure, which allows us to see the actual result of the StringBuffer at this point. This confirms that the class is really working, so now you can change the assertion back into the correct form. Run it again to get "OK", just to make sure everything is working again:
Figure 8: All tests pass!
This step has introduced the Test-First Approach, which is a particular method of software development that is driven by Unit Testing.
- When writing new classes, First design an interface. Second, write tests that define what this interface should be doing. Third, write the actual implementation. The tests clearly define the goal that you want to achieve, and when all the tests pass, you are done. Good job!
- Especially when checking a result, it is good to see a test fail at least once. This will raise confidence that the test is checking the right thing.
- comparison tests should be done with:
m_assert.TypesEqual expected, actual, message