As I said at the beginning, the most important concept that I want this chapter to convey is the necessity of writing testable code. Less experienced Visual Basic programmers have a tendency to implement a lot of functionality directly beneath command buttons. I've certainly seen instances where there are several hundred lines of code behind a button: code that updates the screen, displays and processes the results from a File Open dialog box, reads from the registry, performs a database access, and then writes to the screen again. I've even seen code like this that exists in two places: behind a command button and again underneath a menu item. (Cut and paste can be such a useful facility.)
When you write a piece of code it needs to be as function-specific as possible. Therefore the monolithic code block that I've just described should be broken down into small routines. First of all there should be very little code behind a button—ideally, a call to another routine, but a small number of relatively safe commands is acceptable. If there is a need for a large amount of processing, there should be one procedure that controls the overall flow and control of the process, and that procedure calls function-specific routines. In the description I gave above, the database access should be a separate routine, as should the Registry code, and so on. This is good practice, and there is no taboo in having many small private routines attached to a form, a module, a class, or whatever.3 The testing is so much easier this way, and it also makes for much more specific error handling code. It's also tidier, of course.
It's important not to get too formal with the coding, though; we'd never get it delivered. The code that goes into making a Microsoft Windows application can be divided into two categories: process specific and interface specific. In very general terms4 the test code that goes into the process-specific sections is what needs to be planned beforehand because it's the process that actually gets the user's work done. The interface-specific elements of the system are still important, but they demand a much greater degree of spontaneous interaction from the user.
To illustrate the planning process I have invented the following functional specification for a small DLL, and I have included an associated test plan.5 Most real-world requirements will be more comprehensive than this but I don't feel that additional detail would add any extra weight to the point that I'm trying to get across. All software should be written from a functional specification (or design document, if you prefer). However, you'll find that if you write the test plan at the same time as (or immediately after) the functional specification, you will continually identify test scenarios that will prompt you to go back to the functional specification to add necessary error handling directives. This happened to me while I was writing the test script specification for the example DLL, even though it's only a simple demonstration.
Create a prototype ActiveX DLL component (SERVERDATA.DLL) that encapsulates the StockList table of the SERVERDATA.MDB database. The table is defined as shown on the following page.
Field name | Data Type | Description |
ID | AutoNumber | ID number for each record |
StockCode | Text (length = 8) | Stock code to identify item |
Name | Text (length = 50) | Name of stock item |
StockLevel | Number (Long Integer) | Number of units currently held |
UnitPrice | Currency | Price of each unit |
The following characteristics should be defined for the DLL component:
Implement the following interface:
Add method Input parameters:
StockCode As String Name As String StockLevel As Long UnitPrice As Currency
This method should create a new record in the StockList table and populate that record with the parameter data. It should check that the StockCode value has not already been used and that all numeric values are at least zero. (If there is an error, a negative value is returned.)
Count property (read-only) This property should return the number of records in the StockList table.
Item method function Input parameters:
StockCode As String
This function should create, instantiate, and return an object of type CStockItem for the record identified by the StockCode parameter. This function should raise an error in the event of the record not being found.
ItemList function Input parameters:
None
This function should return a collection of all StockCode values that exist within the StockList table.
StockValue property (read-only)This property should return the sum of each record's StockLevel field multiplied by its UnitPrice field.
Remove method Input parameters:
StockCode As String
This method should delete the record that matches the supplied StockCode value.
Implement the following interface:
Name property (read/write) Let/Get for the Name field.
StockCode property (read/write) Let/Get for the StockCode field.
StockLevel property (read/write) Let/Get for the StockLevel field.
UnitPrice property (read/write) Let/Get for the UnitPrice field.
StockValue property (read-only) Get property only. Calculated dynamically and is the product of the UnitPrice field and the StockLevel field.
Update method This method should apply any changes that are made to any of the read/write properties.
(The idea here is that we want to methodically check each member of the public interface. Some of the test routines will automatically make use of some of the other members.)
Objective: To ensure that each member in the CStockList and CStockItem classes have been run at least once to ensure correct behavior. This is a component-level test that will be performed by the development team.
Test methodology: The two classes are implemented in an ActiveX DLL. This allows for the creation of a dedicated test harness application that will act as a normal client program. For this initial test, sample data will be hard-coded into the test harness application. (The possibility exists to extend this in the future so that test data will be read from a data file.)
Scope of this strategy: This is a generic document that outlines a method of testing without providing any test data. Reasonable test data can be created as required.
Test environment: Windows 98 (full installation), run-time files as installed by Visual Basic 6. No service packs are applied to these products at this time.
Members used: Add, Count, Item, StockValue
Intent: Check that a record is added successfully.
Members used: Add
Intent: Prove that a new record with a duplicate key value will be rejected.
Members used: Remove
Intent: Check that an attempt to delete a record that doesn't exist will fail gracefully.
Members used: Item, Update
Intent: Prove that the CStockItem.Update method will reject an attempt to modify a record where the StockCode value would be the same as an existing record.