Testing modal windows in Delphi
We all do unit testing, right? We write tests for all our classes so we know that they do what we want them to do. Our application is nothing more than a collection of classes and interactions between them. We also test these interactions. So we have a large test suite that tests all classes individually as well as the interactions between them.
In case of desktop applications we should look at descendants of TForm as at just another classes that need to be tested. GuiTesting.pas unit from DUnit may be helpful in this area. The idea is to create and show the form, do some checks and close/free it. In most cases that's enough. Sometimes, however, we have a form that serves as a modal dialog and should be tested as that.
Delphi help says:
"A modal form is one where the application can't continue to run until the form is closed."
So we get stuck. How can we execute our tests while the application can't continue until the form is closed? Ok, we can create it, fill it with some data, call ShowModal(), and before freeing it, we can do our checks. But how are we going to exit the modal state if we are waiting for it to close? Well, we might close it manually but we want our tests to be unattended! So we need a solution that we'll allow us to this automatically.
Let's look first how Delphi applications work.
begin Application.Initialize; Application.CreateForm(TMainForm, MainForm); Application.Run; end
The most important part in this typical Delphi project file (DPR) contents is Application.Run. The main purpose of this method is to handle windows messages:
//(...) repeat try HandleMessage; except HandleException(Self); end; until Terminated; //(...)
And this is how most Windows applications work, regardless of the language used to create them. An application has a loop and waits for messages. The messages mainly come from the operating system itself. When a message arrives, the application handles it and waits for another one until it's terminated.
For example: when we click a button - Windows sends appropriate messages to our application. These messages are intercepted by the main application loop and sent to appropriate controls. In this way the message is finally converted to an event handler of the button in which we can execute our code as a response to this click. We can also send windows messages by ourselves using SendMessage() and PostMessage() from Windows API.
When we tell a form to be shown in the modal way, Delphi disables all other windows of the application and enters another loop. Below are contents of TCustomForm.ShowModal() function (some parts removed to make it more clear):
WindowList := DisableTaskWindows(0); try Show; try ModalResult := 0; repeat Application.HandleMessage; if Application.Terminated then ModalResult := mrCancel else if ModalResult <> 0 then CloseModal; until ModalResult <> 0; Result := ModalResult; finally Hide; end; finally EnableTaskWindows(WindowList); end;
As you see in this loop Application.HandleMessage() is executed too. That means that the application still handles all messages sent by Windows. Moreover, that also means that it would handle all messages that we would send to it. And this gives us a chance to execute our code while the window is in the modal state!
I created a simple library for this very purpose. I called it FutureWindows and you can find it in the public repository of my open source Delphi code. It is basically one unit which you can use in your project to do things like:
uses FutureWindows; procedure TMyTestCase.TestSample; begin TFutureWindows.Expect(TForm.ClassName) .ExecProc( procedure (const AWindow: IWindow) var myForm: TForm; begin myForm := AWindow.AsControl as TForm; myForm.Caption := 'test caption'; myForm.Close(); end ); with TForm.Create(Application) do try Caption := ''; ShowModal(); CheckEquals('test caption', Caption); finally Free; end; end;
Check demos for more examples.
TFutureWindows.Expect() takes one parameter, which is a window class name and returns an instance of IFutureWindow. We provide the future window with an action to execute when a window of interest is found. In the above example I use anonymous method as an action to execute in future. Once we've defined the action, we can show the form in the modal state and wait for our action to be executed.
I implemented this using a hidden window with a windows timer. Windows sends WM_TIMER to the hidden window giving us a chance to find the window we are interested in and do our job. And we can do this thanks to Application.HandleMessage() in the modal message loop.
Having this solution in hand you can build test cases that are closer to how your users interact with your application. These will not be classical unit tests, more integration tests, but the name doesn't really matter. What matters is that you keep writing tests.