In this interesting blog post on delphiXtreme I read about DUnit's built-in GUI testing capabilities (basically an alternative test case class TGUITestCase
defined in unit GUITesting
that has several utility functions for invoking actions in the GUI). I was quite happy with it until I noticed that it didn't work with modal forms. For example the following sequence won't work if the first button shows a modal configuration form:
Click ('OpenConfigButton');
Click ('OkButton');
The second Click
is only executed when the modal form is closed, which I have to do manually.
I don't know much about how modal forms work in the background but there must be some way to circumvent this behaviour. Naively, I want to somehow execute the ShowModal
"in a thread" so that the "main thread" stay responsive. Now I know that running ShowModal
in a thread will probably mess up everything. Are there any alternatives? any way to circumvent the blocking nature of a ShowModal
? Has anybody some experiences with GUI testing in Delphi?
I know about external tools (from QA or others) and we use those tools, but this question is about GUI testing within the IDE.
Thanks!
You can't test modal forms by calling ShowModal
; because as you have quite rightly discovered, that results in your test case code 'pausing' while the modal form awaits user interaction.
The reason for this is that ShowModal
switches you into a "secondary message loop" that does not exit until the form closes.
However, modal forms can still be tested.
Show
method.Show
means the form won't be closed by setting the modal result.ModalResult
is correct.You can use this technique to test a specific modal form by explicitly showing it non-modally. However, any code under test that shows a modal form (e.g. Error Dialog) will pause your test case.
Even your sample code: Click ('OpenConfigButton');
results in ShowModal being called, and cannot be tested in that manner.
To resolve this, you need your "show commands" to be injectible into your application. If you're unfamliar with dependency injection, I recommend Misko Hevery's Clean Code Talks videos available on You Tube. Then while testing, you inject a suitable version of your "show commands" that won't show a modal form.
For example, your modal form may show an error dialog if validation fails when the Ok button is clicked.
So:
1) Define an interface (or abstract base class) to display an error messages.
IErrorMessage = interface
procedure ShowError(AMsg: String);
end;
2) The form you're testing can hold an injected reference to the interface (FErrorMessage: IErrorMessage
), and use it to show an error whenever validation fails.
procedure TForm1.OnOkClick;
begin
if (Edit1.Text = '') then
FErrorMessage.ShowError('Please fill in your name');
else
ModalResult := mrOk; //which would close the form if shown modally
end;
3) The default version of IErrorMessage used / injected for production code will simply display the message as usual.
4) Test code will inject a mock version of IErrorMessage to prevent your tests from being paused.
5) Your tests can now execute cases that would ordinarily display an error message.
procedure TTestClass.TestValidationOfBlankEdit;
begin
Form1.Show; //non-modally
//Do not set a value for Edit1.Text;
Click('OkButton');
CheckEquals(0, Form1.ModalResult); //Note the form should NOT close if validation fails
end;
6) You can take the mock IErrorMessage a step further to actually verify the message text.
TMockErrorMessage = class(TInterfaceObject, IErrorMessage)
private
FLastErrorMsg: String;
protected
procedure ShowError(AMsg: String); //Implementaion trivial
public
property LastErrorMsg: String read FLastErrorMsg;
end;
TTestClass = class(TGUITesting)
private
//NOTE!
//On the test class you keep a reference to the object type - NOT the interface type
//This is so you can access the LastErrorMsg property
FMockErrorMessage: TMockErrorMessage;
...
end;
procedure TTestClass.SetUp;
begin
FMockErrorMessage := TMockErrorMessage.Create;
//You need to ensure that reference counting doesn't result in the
//object being destroyed before you're done using it from the
//object reference you're holding.
//There are a few techniques: My preference is to explicitly _AddRef
//immediately after construction, and _Release when I would
//otherwise have destroyed the object.
end;
7) Now the earlier test becomes:
procedure TTestClass.TestValidationOfBlankEdit;
begin
Form1.Show; //non-modally
//Do not set a value for Edit1.Text;
Click('OkButton');
CheckEquals(0, Form1.ModalResult); //Note the form should NOT close if validation fails
CheckEqulsString('Please fill in your name', FMockErrorMessage.LastErrorMsg);
end;
There is actually a way to test modal windows in Delphi. When a modal window is shown your application still processes windows messages so you could post a message to some helper window just before showing the modal window. Then your message would be handled from the modal loop allowing you to execute code while the modal window is still visible.
Recently I've been working on a simple library to handle this very problem. You can download the code from here: https://github.com/tomazy/DelphiUtils (see: FutureWindows.pas).
Sample usage:
uses
Forms,
FutureWindows;
procedure TFutureWindowsTestCase.TestSample;
begin
TFutureWindows.Expect(TForm.ClassName)
.ExecProc(
procedure (const AWindow: IWindow)
var
myForm: TForm;
begin
myForm := AWindow.AsControl as TForm;
CheckEquals('', myForm.Caption);
myForm.Caption := 'test caption';
myForm.Close();
end
);
with TForm.Create(Application) do
try
Caption := '';
ShowModal();
CheckEquals('test caption', Caption);
finally
Free;
end;
end;
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With