Delphi GUI Testing and Modal Forms

2019-01-23 05:32发布

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!

2条回答
一纸荒年 Trace。
2楼-- · 2019-01-23 05:56

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.

  1. Show the usually Modal form using the normal Show method.
  2. This allows your test case code to continue, and simulate user actions.
  3. These actions and effects can be tested as normal.
  4. You will want an additional test quite particular to Modal forms:
    1. A modal form is usually closed by setting the modal result.
    2. The fact that you used Show means the form won't be closed by setting the modal result.
    3. Which is fine, because if you now simulate clicking the "Ok" button...
    4. You can simply check that the ModalResult is correct.

WARNING

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;
查看更多
一纸荒年 Trace。
3楼-- · 2019-01-23 06:15

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;
查看更多
登录 后发表回答