approaches to automatically testing visual components richard b. winston cpcug programmers sig nov....
TRANSCRIPT
Approaches to Approaches to Automatically Testing Automatically Testing Visual ComponentsVisual Components
Richard B. WinstonRichard B. Winston
CPCUG Programmers SIGCPCUG Programmers SIG
Nov. 1, 2006Nov. 1, 2006
OutlineOutline
• Review of DUnitReview of DUnit
• Difficulties Using Dunit with Visual Difficulties Using Dunit with Visual ComponentsComponents
• Simulating User Interaction in DUnitSimulating User Interaction in DUnit
• Comparing Appearances of Visual Comparing Appearances of Visual ComponentsComponents
• LimitationsLimitations
Review of DUnitReview of DUnit
• Testing Framework for DelphiTesting Framework for Delphi
• Open Source: Open Source: http://dunit.sourceforge.net/http://dunit.sourceforge.net/
• Built-in support in BDS 2005 and Built-in support in BDS 2005 and 2006 2006
• Works with Delphi >= 5Works with Delphi >= 5
DUnit Example: 1DUnit Example: 1
typetype // Test methods for class TRbwParser// Test methods for class TRbwParser TestTRbwParser = class(TTestCase)TestTRbwParser = class(TTestCase) strict privatestrict private FRbwParser: TRbwParser;FRbwParser: TRbwParser; publicpublic procedure SetUp; override;procedure SetUp; override; procedure TearDown; override;procedure TearDown; override; publishedpublished procedure TestClearExpressions;procedure TestClearExpressions; … … end;end;
DUnit Example: 2DUnit Example: 2
procedure TestTRbwParser.TestClearExpressions;procedure TestTRbwParser.TestClearExpressions;varvar Expression: string;Expression: string;beginbegin if FRbwParser.ExpressionCount = 0 thenif FRbwParser.ExpressionCount = 0 then beginbegin Expression := '1+2';Expression := '1+2'; FRbwParser.Compile(Expression);FRbwParser.Compile(Expression); end;end; Check(FRbwParser.ExpressionCount > 0, 'Error in counting Check(FRbwParser.ExpressionCount > 0, 'Error in counting
expressions');expressions');
FRbwParser.ClearExpressions;FRbwParser.ClearExpressions; Check(FRbwParser.ExpressionCount = 0, 'Error in clearing Check(FRbwParser.ExpressionCount = 0, 'Error in clearing
expressions');expressions');end;end;
DUnit in ActionDUnit in Action
Manually Testing User Manually Testing User InteractionInteraction
• ManualManual– Ask the user to do Ask the user to do
somethingsomething– Let the user decide Let the user decide
if the results are if the results are correctcorrect
• ProblemsProblems– SlowSlow– UnreliableUnreliable– Discourages TestingDiscourages Testing
Automatically Test User Automatically Test User InteractionInteraction
• Simulate Mouse and Keyboard EventsSimulate Mouse and Keyboard Events• Get Screen Captures of the ControlsGet Screen Captures of the Controls• Compare the New Screen Captures to Compare the New Screen Captures to
Old OnesOld Ones• Borland’s private “Zombie” program Borland’s private “Zombie” program
does something similar. See does something similar. See http://www.stevetrefethen.com/files/inhttp://www.stevetrefethen.com/files/index.htmldex.html
Windows API for Simulating Windows API for Simulating the Mouse and Keyboardthe Mouse and Keyboard
• Mouse_Event( MOUSEEVENTF_LEFTMouse_Event( MOUSEEVENTF_LEFTDOWN, 0, 0, 0, 0);DOWN, 0, 0, 0, 0);
• Keybd_Event(key, Keybd_Event(key, MapvirtualKey(key, 0), flag, 0)MapvirtualKey(key, 0), flag, 0)
Simulate Mouse and Simulate Mouse and KeyboardKeyboard
// move the mouse to cell [0,1]// move the mouse to cell [0,1]
APoint := CellCenter(frmGridTest.TestGrid, 0,1);APoint := CellCenter(frmGridTest.TestGrid, 0,1);
MoveMouseToPosition(APoint);MoveMouseToPosition(APoint);
// double click on the cell to start editing text.// double click on the cell to start editing text.
DoubleClick;DoubleClick;
// enter some text.// enter some text.
SimultateText('abcd efgh ijkl');SimultateText('abcd efgh ijkl');
CellCenterCellCenter
function CellCenter(Const Grid: TCustomDrawGrid; function CellCenter(Const Grid: TCustomDrawGrid; Const ACol, ARow: integer): TPoint;Const ACol, ARow: integer): TPoint;
varvar ARect: TRect;ARect: TRect;beginbegin ARect := Grid.CellRect(ACol,ARow);ARect := Grid.CellRect(ACol,ARow); result.X := (ARect.Left + ARect.Right) div 2;result.X := (ARect.Left + ARect.Right) div 2; result.Y := (ARect.Top + ARect.Bottom) div 2;result.Y := (ARect.Top + ARect.Bottom) div 2; result := Grid.ClientToScreen(result);result := Grid.ClientToScreen(result);end;end;
MoveMouseToPositionMoveMouseToPosition
Procedure Procedure MoveMouseToPosition(Position: MoveMouseToPosition(Position: TPoint);TPoint);
BeginBegin
Mouse.CursorPos := Position;Mouse.CursorPos := Position;
Application.ProcessMessages;Application.ProcessMessages;
end;end;
MouseClickMouseClick
procedure MouseGoesDown;procedure MouseGoesDown;beginbegin
Mouse_Event( MOUSEEVENTF_Mouse_Event( MOUSEEVENTF_LEFTDOWN, 0, 0, 0, 0);LEFTDOWN, 0, 0, 0, 0);
end;end;
procedure MouseGoesUp;procedure MouseGoesUp;beginbegin
Mouse_Event( MOUSEEVENTF_LMouse_Event( MOUSEEVENTF_LEFTUP, 0, 0, 0, 0);EFTUP, 0, 0, 0, 0);
end;end;
procedure MouseClick;procedure MouseClick;beginbegin MouseGoesDown;MouseGoesDown; MouseGoesUp;MouseGoesUp;end;end;
procedure DoubleClick;procedure DoubleClick;beginbegin MouseClick;MouseClick; MouseClick;MouseClick;end;end;
DragMouseDragMouse
procedure DragMouse(Start, Stop: TPoint);procedure DragMouse(Start, Stop: TPoint);ConstConst TenthSecond = 100; // 100 milliseconds - 1/10 second. This value works for me.TenthSecond = 100; // 100 milliseconds - 1/10 second. This value works for me.beginbegin MoveMouseToPosition(Start);MoveMouseToPosition(Start); MouseGoesDown;MouseGoesDown; trytry MoveMouseToPosition(Stop);MoveMouseToPosition(Stop); // To generate a mouse move event, enough time must elapse.// To generate a mouse move event, enough time must elapse. // Empirically 1/20'th of a second is enough.// Empirically 1/20'th of a second is enough. Sleep(TenthSecond);Sleep(TenthSecond); Application.ProcessMessages;Application.ProcessMessages; finallyfinally MouseGoesUp;MouseGoesUp; Application.ProcessMessages;Application.ProcessMessages; end;end;end;end;
SimultateTextSimultateText
Procedure SimultateText(const AString: Procedure SimultateText(const AString: string);string);
varvar CharIndex: integer;CharIndex: integer; AChar: Char;AChar: Char; KeyCode: TKeyCode;KeyCode: TKeyCode; Shift: TShiftState;Shift: TShiftState;beginbegin for CharIndex := 1 to Length(AString) dofor CharIndex := 1 to Length(AString) do beginbegin AChar := AString[CharIndex];AChar := AString[CharIndex];
KeyCode.LongKeyCode := KeyCode.LongKeyCode := VkKeyScan(AChar);VkKeyScan(AChar);
Shift := [];Shift := [];
if (KeyCode.Shift and 1) <> 0 thenif (KeyCode.Shift and 1) <> 0 then beginbegin Include(Shift, ssShift);Include(Shift, ssShift); end;end; if (KeyCode.Shift and 2) <> 0 thenif (KeyCode.Shift and 2) <> 0 then beginbegin Include(Shift, ssCtrl);Include(Shift, ssCtrl); end;end; if (KeyCode.Shift and 4) <> 0 thenif (KeyCode.Shift and 4) <> 0 then beginbegin Include(Shift, ssAlt);Include(Shift, ssAlt); end;end;
PostKeyEx32(KeyCode.KeyCode, Shift, PostKeyEx32(KeyCode.KeyCode, Shift, False);False);
end;end;end;end;
PostKeyEx32PostKeyEx32// from // from
http://delphi.about.com/od/adptips200http://delphi.about.com/od/adptips2004/a/bltip0604_4.htm4/a/bltip0604_4.htm
procedure PostKeyEx32(key: Word; const procedure PostKeyEx32(key: Word; const shift: TShiftState; specialkey: Boolean) shift: TShiftState; specialkey: Boolean) ;;
typetype TShiftKeyInfo = recordTShiftKeyInfo = record shift: Byte ;shift: Byte ; vkey: Byte ;vkey: Byte ; end;end; ByteSet = set of 0..7 ;ByteSet = set of 0..7 ;constconst shiftkeys: array [1..3] of TShiftKeyInfo =shiftkeys: array [1..3] of TShiftKeyInfo = ((shift: Ord(ssCtrl) ; vkey: ((shift: Ord(ssCtrl) ; vkey:
VK_CONTROL),VK_CONTROL), (shift: Ord(ssShift) ; vkey: VK_SHIFT),(shift: Ord(ssShift) ; vkey: VK_SHIFT), (shift: Ord(ssAlt) ; vkey: VK_MENU)) ;(shift: Ord(ssAlt) ; vkey: VK_MENU)) ;varvar flag: DWORD;flag: DWORD; bShift: ByteSet absolute shift;bShift: ByteSet absolute shift; j: Integer;j: Integer;
beginbegin for j := 1 to 3 dofor j := 1 to 3 do beginbegin if shiftkeys[j].shift in bShift thenif shiftkeys[j].shift in bShift then keybd_event(shiftkeys[j].vkey, keybd_event(shiftkeys[j].vkey, MapVirtualKey(shiftkeys[j].vkey, 0), 0, 0) ;MapVirtualKey(shiftkeys[j].vkey, 0), 0, 0) ; end;end; if specialkey thenif specialkey then flag := KEYEVENTF_EXTENDEDKEYflag := KEYEVENTF_EXTENDEDKEY elseelse flag := 0;flag := 0;
keybd_event(key, MapvirtualKey(key, 0), flag, 0) ;keybd_event(key, MapvirtualKey(key, 0), flag, 0) ; flag := flag or KEYEVENTF_KEYUP;flag := flag or KEYEVENTF_KEYUP; keybd_event(key, MapvirtualKey(key, 0), flag, 0) ;keybd_event(key, MapvirtualKey(key, 0), flag, 0) ;
for j := 3 downto 1 dofor j := 3 downto 1 do beginbegin if shiftkeys[j].shift in bShift thenif shiftkeys[j].shift in bShift then keybd_event(shiftkeys[j].vkey, keybd_event(shiftkeys[j].vkey, MapVirtualKey(shiftkeys[j].vkey, 0), MapVirtualKey(shiftkeys[j].vkey, 0), KEYEVENTF_KEYUP, 0) ;KEYEVENTF_KEYUP, 0) ; end;end;end;end;
Comparing Appearances of Comparing Appearances of Visual ComponentsVisual Components PaintControlToBitMap(frmGridTest.TestGrid, PaintControlToBitMap(frmGridTest.TestGrid, TestBitMap, True);TestBitMap, True); frmGridTest.Hide;frmGridTest.Hide;
BitmapKey := BitmapKey := RbwDataGridAutoWordWrapAdjustRowHeight;RbwDataGridAutoWordWrapAdjustRowHeight; UserPrompt := UserPrompt := 'Has the text in the string and boolean cells word 'Has the text in the string and boolean cells word
wrapped 'wrapped ' + 'and have the rows expanded to accommodate the + 'and have the rows expanded to accommodate the
text?';text?'; Result := CompareAndUpdateBitmap(BitmapKey, Result := CompareAndUpdateBitmap(BitmapKey,
UserPrompt, UserPrompt, TestBitMap);TestBitMap);
PaintControlToBitMapPaintControlToBitMap
Procedure PaintControlToBitMap(Control: TControl;Procedure PaintControlToBitMap(Control: TControl; BitMap: TBitMap; IncludeCursor: boolean);BitMap: TBitMap; IncludeCursor: boolean);beginbegin// Set the size of the bitmap that will hold the image of Control// Set the size of the bitmap that will hold the image of Control BitMap.Width := Control.Width;BitMap.Width := Control.Width; BitMap.Height := Control.Height;BitMap.Height := Control.Height;
if Control is TWinControl thenif Control is TWinControl then beginbegin TWinControl(Control).PaintTo(BitMap.Canvas, 0, 0);TWinControl(Control).PaintTo(BitMap.Canvas, 0, 0); endend else if Control is TGraphicControl thenelse if Control is TGraphicControl then beginbegin // Get a screen capture of the form containing the TGraphicControl// Get a screen capture of the form containing the TGraphicControl // and copy only the portion of that screen capture that contains// and copy only the portion of that screen capture that contains // the TGraphicControl into BitMap.// the TGraphicControl into BitMap.
CompareAndUpdateBitmapCompareAndUpdateBitmap
function function CompareAndUpdateBitmap(const CompareAndUpdateBitmap(const BitmapKey, UserPrompt: string;BitmapKey, UserPrompt: string;
TestBitMap: TBitmap): Boolean;TestBitMap: TBitmap): Boolean;varvar PredefinedBitMap: TBitmap;PredefinedBitMap: TBitmap;beginbegin PredefinedBitMap := PredefinedBitMap :=
FBitmapStorage.BitMapCollection.FBitmapStorage.BitMapCollection.Find(BitmapKey);Find(BitmapKey);
result := result := CompareBitMaps(TestBitMap, CompareBitMaps(TestBitMap, PredefinedBitMap);PredefinedBitMap);
if not result and (TestMethod = if not result and (TestMethod = tmScripted) thentmScripted) then
beginbegin result := result :=
UserCompareBitMap(PredefinedBiUserCompareBitMap(PredefinedBitMap, TestBitMap, UserPrompt);tMap, TestBitMap, UserPrompt);
if result thenif result then beginbegin if PredefinedBitMap = nil thenif PredefinedBitMap = nil then beginbegin FBitmapStorage.FBitmapStorage.
BitMapCollection.Store(BitmapKey,BitMapCollection.Store(BitmapKey, TestBitMap);TestBitMap); endend elseelse beginbegin PredefinedBitMap.Assign(PredefinedBitMap.Assign( TestBitMap);TestBitMap); end;end; end;end; end;end;end;end;
LimitationsLimitations
• Someone has to check the Someone has to check the appearance of a control manually at appearance of a control manually at least once.least once.
• There can be no manual use of the There can be no manual use of the mouse or keyboard during a test.mouse or keyboard during a test.
• Changes to the computer outside of Changes to the computer outside of the control of the test, such as the control of the test, such as changing from small to large fonts, changing from small to large fonts, will affect the bitmap comparisons.will affect the bitmap comparisons.
SummarySummary
• DUnit can be used to test visual DUnit can be used to test visual controls.controls.
• Manual user interaction during tests Manual user interaction during tests can be reduced but not entirely can be reduced but not entirely eliminated.eliminated.
• The tests are fragile – They are easily The tests are fragile – They are easily affected by changes to the computer affected by changes to the computer outside the test.outside the test.