Delphi GUI Testing and Modal Forms

前端 未结 2 510
执念已碎
执念已碎 2020-12-15 08:37

In this interesting blog post on delphiXtreme I read about DUnit\'s built-in GUI testing capabilities (basically an alternative test case class TGUITestCase def

2条回答
  •  太阳男子
    2020-12-15 09:14

    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;
    

提交回复
热议问题