Back to TOC SOFTWARE TOOLS


An Automated Testing Tool for Win16

Paul Carlson

Regression testing demands reproducible inputs, which is no mean feat if the inputs are ordinarly generated by users working a keyboard and mouse.


Most developers would probably agree that testing software is often a difficult, time-consuming ordeal. Testing tools can dramatically improve the speed and quality of the testing process, but they can also be very expensive, running from hundreds to thousands of dollars for a single copy. Fortunately, it's possible to create your own testing tools through the magic of Windows hook functions. Using hook functions, it's easy to build an automated testing library that can capture mouse and keyboard input to be played back later. The ability to play back inputs, thus simulating interaction with a user, is quite handy for debugging Windows applications.

In this article I show how to use the Windows hook functions to build an automated testing library. I've used the testing library presented in this article with both Powerbuilder and C/C++ applications. Once test cases have been recorded, they can be replayed over and over again. I've used this tool to track down everything from memory leaks to database lock contention.

I have tested this tool with real applications running on Windows NT, Windows 95, and Windows 3.x. Naturally, this article assumes you have a basic knowledge of Windows programming. If you need more information about Windows programming, Petzold's landmark publication, Programming Windows 3.1 [1] , is a good place to start.

Hooking into Windows

A hook, a.k.a. callback function, is essentially an application-defined function that is called by Windows via a pointer-to-function mechanism. Windows allows an application to install a variety of different hooks, but the two of interest here are the WH_JOURNALRECORD and WH_JOURNALPLAYBACK hooks.

Since these are system-wide hook functions, they must be placed into a Windows DLL. This article shows only how to build a Windows 16-bit DLL, but these functions also exist in the Win32 API. With minor modifications, you should be able to build a 32-bit version of this library. In fact, if you use the Win32 API, you need not build a separate DLL for your testing library; it can be compiled directly into your application.

The first thing to do is declare the hook functions. A hook function must have the following signature:

LRESULT CALLBACK functionName(int code,
   WPARAM wParam, LPARAM lParam).

In the example DLL I have defined two hook functions, one for recording messages and one for message playback. I call these functions JournalRecordProc and JournalPlaybackProc (see Listing 1) . I've chosen these names for a very good reason. They are the placeholder names that describe how playback and record hook functions should work as defined by the Windows 3.1 API. For example, if you search the help text on "JournalRecordProc," you will get a description on how this function should work. Of course, you may call these functions something else if you choose. Additionally, make sure that you export these functions. In my example, I have added the _export keyword to explicitly specify that these are indeed exported functions. I will describe the code that goes into these functions a little later.

The next thing to do is define an exportable function to start recording. The function startRecording appears in Listing 2. This function performs some basic file setup routines, but its main purpose is to install the JournalRecordProc hook procedure. startRecording first retrieves the address of the exported hook function via a call to GetProcAddress. startRecording then installs the hook function via a call to SetWindowsHookEx. The WH_JOURNALRECORDPROC parameter is a flag specifying that this is a journal recording hook. Windows uses this flag to determine how often to call the hook function based on the particular message that Windows is processing at the time. Once the call to SetWindowsHookEx completes, the hook is installed and will be called during Windows message processing.

The third thing to do is to define an exportable function to start playback. The process is pretty much the same as for setting the JournalRecordProc hook described above. The function to install the playback hook is called startPlayback. The arguments to this function specify the script to play back, and how many times to playback that particular script. As before, the main point of this function is to install a hook, in this case JournalPlaybackProc. When startPlayback calls SetWindowsHookEx, it passes the WH_JOURNALPLAYBACKPROC flag as the first parameter.

Callback Function Specifics

Now for the details of the record callback function (JournalRecordProc, also in Listing 2) . This function takes three parameters. The last parameter is the address of a Windows message; the first parameter is a code that indicates whether the function needs to process (record) that message. A negative code indicates that some system event has occurred, and that this function should not record the message but pass it on to the next callback function. The second parameter, wParam, is undefined.

If the code is not negative, the record function performs some specific action based on the code. For example, an HC_ACTION code causes the function to write the message received and milliseconds elapsed since the last message into a log file [2] . The need for writing out the milliseconds elapsed will become clear when I describe the playback process.

If the code parameter is HC_SYSMODALON, the record function stops recording messages, since it is not appropriate to record messages that are really responses to system modal dialogs. Once the function is passed an HC_SYSMODALOFF, it can start recording again.

Finally, the record callback function returns a zero to provide behavior, defined in the Windows API.

The playback function (JournalPlaybackProc) works in a very similar fashion as the record function. The lParam argument still points to an EVENTMSG structure, but in this case the caller must fill the structure with the message to be processed.

Again, the code parameter indicates how to process the message, but this time the message will come from the log file, not from the parameter list.

If the code is of type HC_SKIP, then the playback function simply reads the message and delay time from the log file and saves them in static variables. The playback function will use these stored values later. After reading from the log file, the playback function returns 0.

When the playback function receives a code of HC_GETNEXT, it puts the previously saved message into the EVENTMSG structure pointed to by the lParam parameter, and passes the amount of time to delay as the function return value. The delay time is in milliseconds. I add 10 milliseconds just to ensure that dynamic application behaviors (i.e. slow network access, unusually slow disk activity, etc) don't throw off the message processing. You can speed up message processing by simply subtracting a few milliseconds from this value, or you may also choose to remove it completely.

Finally, if the playback function receives a code equal to either HC_SYSMODALON or HC_SYSMODALOFF, it just forwards the message on to the next hook function.

Unhooking from Windows

Since hook functions are limited system-wide resources, it's important to remove them when they're not in use. The stopRecording and stopPlayback functions simply remove the hook functions via the UnhookWindowsHookEx function.

One final note regarding this DLL. I open and close the log file with each message read or written. While this does incur some file I/O overhead, it ensures that file handles are not left open and that everything meant to be recorded is actually written to the log file. As I will show, it is possible to interrupt the playback process by simply pressing CTRL-ESC. It's not a good idea to leave the log file open if playback is interrupted (or if the application crashes and the DLL remains loaded).

Using the DLL

To use the DLL in an application, simply import the startRecording, stopRecording, and startPlayback functions. (You'll need to download the resource file, sample.rc, and the definition file, sample.def, from the CUJ ftp site. See p.3 for downloading details.) I typically create menu items that correspond to each activity and call the corresponding imported function when the menu item is clicked. For example, I call the startRecording function when the user clicks on "Start Recording." At that time, the JournalRecordProc hook procedure gets installed and system activity starts to be recorded. Recording will stop when the user clicks on the "Stop recording" menu item.

Playback of the recorded script will start when the user clicks on "Start Playback." While the script is playing, regular mouse and keyboard input is disabled. If you need to interrupt script playback, simply press CTRL-ESC. The script will be repeated based on the value passed into the nIterations parameter. I've included a bare-bones sample application with this article as an example (see Listing 3) .

For a few hundred lines of code, you now have an automated testing tool. Of course, this tool does have some limitations. First, it records application level activity. As soon as the current application loses focus, the DLL will stop recording. A more powerful system-wide testing tool could be written using some of the other hook functions. However, I have run into only a few instances where this was a problem. Additionally, since the tool works based on screen position, any changes made to the user interface may invalidate the testing scripts that you've recorded. Since it is so easy to record your scripts, you should be able to re-create them quickly.

I've found this tool to be extremely useful and versatile. It is easy to distribute and put on hundreds or even thousands of computers. This makes it easier to test things like resource contention, response time, and application scalability. Additionally, it is very useful for regression testing. I would appreciate hearing of your uses for this tool. o

Notes

[1] Charles Petzold. Programming Windows 3.1 (Microsoft Press, 1992). ISBN

1-55615-395-3 [Note: This edition of the book is, unfortunately, out of print. The newest edition focuses primarily on Win32 programming. mb]

[2] The Windows 3.x API states that the address of the message stored in the lParam parameter is of type MSG. This is incorrect. It is really of type EVENTMSG. The Win32 API corrects this mistake.

Paul Carlson has a B.S. in Computer Science from the University of Wisconsin, River Falls. He is currently working on his Masters in Software Engineering at the University of St. Thomas. He has nine years of professional software development experience, and has used C/C++ for the past seven years. He is currently employed as a Specialist with the St. Paul Companies, a large property/liability insurance carrier located in St. Paul, MN. He may be reached at pcarlson@ix.netcom.com.