‏הצגת רשומות עם תוויות VBScript. הצג את כל הרשומות
‏הצגת רשומות עם תוויות VBScript. הצג את כל הרשומות

יום חמישי, 24 במאי 2007

עבודה עם ממשקים (Interfaces) ב-QTP


היום נשחק במשחק החביב עלי – לרדת על VBScript. ספציפית נעסוק בחוסר היכולת של VBScript לעבוד עם אובייקטים מסוימים העטופים בממשק (Interface). לאחרונה הבנתי (בין היתר בעקבות שאלה ב-SQAForums) שהרבה אנשים נתקלו בבעיה הזאת, מבלי להיות מודעים לעומק שלה, והחלטתי שהגיע הזמן לכתוב באופן מסודר איך אני מתמודד איתה.

לפני הכל קצת רקע: VBScript אמנם נתפסת כשפה דלה ועלובה ביחס לשפות תכנות "אמיתיות", אולם בפועל היא יכולה לעבוד עם אובייקטים מתוחכמים שנכתבו בשפות עילית רבות. כך שאם לדוגמה יש לנו אובייקט עם ממשק COM, ניתן ליצור אותו ב-VBScript באמצעות פקודת CreateObject, ולהשתמש בו בלי בעיה. באופן דומה, אם יש לנו DLL שנכתב ב-.Net, ניתן ליצור אותו ב-QTP באמצעות פקודת DotNetFactory, ולעבוד איתו ב-VBScript באופן מלא. הקלות הזו עשויה לגרום לנו להאמין שאין סוג אובייקט ש-VBScript לא יכולה לעבוד איתו, ואולי העובדה ש-QTP מבוסס VBScript היא לא בעיה כל-כך גדולה.

לצערנו, זוהי טעות. ישנם אובייקטים שמורכבים מידי ל-VBScript, וכפועל יוצא לא ניתן לעבוד איתם ב-QTP. מדובר בסוג מסויים של ממשקים (Interfaces), העוטפים את האובייקטים האמיתיים באפליקציה, ומסתירים אותם מ-VBScript. אם המונח Interface לא מוכר לכם, אל תדאגו – אין צורך בכך על מנת להבין את עצם הבעיה (ואם הוא כן מוכר לכם, אל תזדעזעו מכך שאני מתייחס אליהם כאל סוג של אובייקטים – הכל נעשה למען הפשטות והבהירות). בשורה התחתונה, הרבה אפליקציות מורכבות עובדות עם סוג האובייקטים הזה, והמשמעות היא שיש הרבה אפליקציות שעולמן הפנימי נותר חסום בפנינו.

דוגמה אחת לבעייתיות הזאת הפריעה לי במשך כמעט שנה. כאשר מנסים לכתוב אוטומציה לאפליקציות מבוססות ESRI (גיאוגראפיות), זקוקים לאורדינאטות של האזור המוצג במפה. וזה חבל, כי אובייקט המפה הוא בדיוק מסוג האובייקטים הבעייתיים שלנו, ולכן לא משנה כמה ננסה, לא נצליח להשיג את המידע הדרוש.

איך אפשר לדעת האם האובייקט שאתם מנסים לגשת אליו הוא אובייקט מסוג זה? הדרך המהירה ביותר היא לנסות לגשת אליו מתוך QTP. רק צריך להריץ תסריט ממנו אפשר להגיע לאובייקט הרלוונטי, ולעצור אותו (באמצעות Break Point). נכנס לחלון ה-Debug, ונבקש לראות את האובייקט שם. בשלב זה נראה שמדובר באובייקט רגיל, היות ו-QTP מציג את הערך שלו כ- (Object). אבל אם ננסה לגשת לאחת התכונות הפנימיות של האובייקט (לדוגמה QTPObject.Object.SuspectROObject.SomeProperty), נגלה ש-QTP זורק שגיאת "Object Needed). כלומר, QTP יודע שמדובר באובייקט, אבל לא מסוגל "לדבר" איתו. אובייקטים מסוג זה יופיעו כסוג-משתנה 13 (VarType 13 – vbDataObject).

אמנם, כיום מדובר בבעיה לא נפוצה, המופיע בעיקר במערכות מורכבות מאוד. אולם, בהתחשב בכיוון אליו הולכות שפות התכנות הפופולאריות, סביר להניח ש בשנים הקרובות היא תהפוך לבעיה מרכזית יותר ויותר. נראה כיצד ניתן להתגבר עליה. אני מצאתי שלושה מעקפים:

לנסות לגרום לזה לעבוד: כאן מגיע גילוי נאות – אני לא יודע ש-VBScript לא מסוגל לעבוד עם האובייקטים האלו, אלא וויתרתי על למצוא דרך לעשות זאת. לקח לי כמעט שנה להתייאש, ואני ממש אשמח לשמוע שמישהו הצליח למצוא פתרון אמיתי לבעיה.

לכתוב הרחבת .Net ל-QTP (Extensibility): ה-Add-in של .Net מאפשר לכתוב הרחבות לאובייקטי .Net לא מוכרים. ההרחבות מבוססות C#, ולכן הן חופשיות מהמגבלות והבעיות של VBScript. ספציפית הן גם יכולות לעבוד עם האובייקטים הבעייתים. מידע נוסף בנושא כתיבת הרחבות .Net אפשר למצוא בקבצי העזרה של QTP.

לכתוב פונקציה ב-DLL חיצוני: המעקף הזה דומה לכתיבת הרחבה, אבל הוא פשוט בהרבה וקל לביצוע. אפשר לכתוב פונקציה ב-.Net המקבלת אובייקט בעיתי, עובדת איתו, מפיקה ממנו את המידע הדרוש לנו, ומחזירה אותו באריזה ש-VBScript יודע להתמודד איתה. לרוב זאת משימה אפשרית, היות ובשורה התחתונה, המידע הדרוש הוא כמעט תמיד מספר, מחרוזת או מערך (לדוגמה, אם האובייקט שלנו הוא נקודה גיאוגראפית, המידע הדרוש הוא שני מספרים – אורדינאטות X ו-Y, וניתן להבנות אותו כמערך).

ברגע שהפונקציה כתובה וארוזה כ-DLL, אפשר ליצור אותה בתוך QTP (באמצעות פקודת DotNetFactory), ומרגע זה אין בעיה לקרוא לפונקציה, להעביר אליה את האובייקט הבעייתי כפרמטר, ולקבל ממנה את המידע הדרוש.

יום חמישי, 17 במאי 2007

נקודות יציאה מרובות


קל מאוד לשלוט בנקודה בה תסריט הבדיקה נכנס ל-Action / פונקציה – אחרי הכל, יש רק אופציה אחת – מלמעלה. שליטה בנקודות היציאה, זה כבר סיפור שונה לחלוטין - ל-Action יכולה להיות נקודת יציאה אחת, שתיים, או עשר, הכל בהתאם ללוגיקה הפנימית והמבנה שלה. היום נבחן איך ניתן לשלוט בנקודות יציאה מרובות ב-Actions ופונקציות, ולהפוך את הקוד שלנו לקל יותר לתחזוקה ותיקון.

כשהתסריט מתקדם כצפוי, זרימת הקוד פשוטה יחסית. גם כשיש בקוד בלולאות, תנאי IF ובחינת מקרים באמצעות Select Case, הקוד מתבצע בקו ישר יחסית, ומסתיים בשורה האחרונה בתסריט. במקרה כזה יש רק שער יציאה אחד, והכל טוב ויפה. אבל, במקרים בהם התסריט נתקבל בבעיות, כמו במקרה של שגיאת מערכת, או כשל בלוגיקה העסקית של האפליקציה, אין מנוס מיציאה מיידית מהקוד הנוכחי. אחרי הכל, אין שום טעם להמשיך נסות להזין 20 שדות, אם הטופס שמכיל אותם בכלל לא נפתח.

למזלנו, האנשים הנחמדים ב-Mercury (והיום HP) הבינו את הצורך ביציאה מיידית מ-Action, והכינו עבורנו את פקודות ExitAction ו-ExitActionIteration. לרוב השימוש בהן נראה כך:


'….Action Code
If CritialCondition = False Then ExitAction
'….Continue Action



אבל זאת דרך מאוד לא ברורה לצאת באמצע ביצוע משימה. רצוי שנוסיף כתיבה ללוג:


If CritialCondition = False Then
Reporter.ReportEvent MicFail, "Something bad has happened", "Aborting"
ExitAction
End If



בעצם, יש לנו גם כמה אובייקטים שצריך לנקות מהזכרון:


If CritialCondition = False Then
   Reporter.ReportEvent MicFail, "Something bad has happened", "Aborting"
   oFile.Close
   Set oFile = Nothing
   ExitAction
End If



ורגע, יש מידע שצריך לשתול ב-DataTable:


If CritialCondition = False Then
  
Reporter.ReportEvent MicFail, "Something bad has happened",
"Aborting"
  
oFile.Close
   Set oFile = Nothing
   DataTable("out_EntityID", dtlocalsheet) = sEntityID
  
'More here
  
ExitAction
End If




אני מניח שתפסתם את הרעיון. במקום פקודה פשוטה, יש לנו קטע קוד מורכב למדי, שמופיע שוב ושוב בכל נקודות היציאה. המשמעות היא שיש לנו קוד כפול שנצטרך לתחזק באופן שוטף. קוד כפול הוא התגלמות זרע השטן בעולם הדיגיטאלי, ואתם ממש לא רוצים משהו כזה בקוד שלכם. באופן רציני יותר, כפילויות קוד כאלו הן מתכון בטוח לבאגים קשים לאיתור, עבודה מיותרת ותסכול, וצריך להמנע מהן בכל מחיר. אחת הדרכים להמנע מכפילויות הקוד היא לארוז אותו במרוכז בפונקציית יציאה. זוהי הפונקציה היחידה ש"מותר לה" (לפחות כפי שאני כותב קוד) להיות בתוך Action, ולא בקובץ חיצוני. להלן דוגמה לשימוש בפונקציה, כמודגם ב-Action Template שלי:


Dim sResult 'I store result data and values to be reported up the action-call chain
Dim sActionReport
'Instead of flooding the log with inner-action messages, I store them, and report all of them at the exit gate.
'Errors are still reported on-the-fly

'Action code goes here

'All the exit gates execute only one command : ActionEnd.
'It receives two parameters: Boolean for Pass/Fail, string for the exit reason
If CriticalCondition = False Then Call ActionEnd(False, "Reason for exiting")

'Rest of action

'Even the normal successful action exit is managed through ActionEnd, so the last line in every action is:

Call ActionEnd(True, "Action successful")

Sub ActionEnd(bStatus, sReason)
  
'Report details
  
Reporter.ReportEvent MicGeneral, "Inner Action Logs", sActionReport

   If bStatus = True Then
      Reporter.ReportEvent MicFail, "An error has occurred", sReason
   Else
      Reporter.ReportEvent MicPass, "Action successful",
"See inner logs for details"
  
End if

   'Plant datatable info for action-call chain
   DataTable("out_Status", dtlocalsheet) = bStatus
   DataTable("out_Result", dtlocalsheet) = sResult
  
'More if needed

   'Close objects and set to nothing here

   'Other needed exit code

   ExitActionIteration
End Sub


כפי שראינו, שימוש בפונקציית יציאה מרכזית מאפשר לשמור על קוד נקי ופשוט, ולמנוע הצפות לוג. טיפלנו בנקודות יציאה מ-Action, אבל מה לגבי יציאה מפונקציה? כמובן שאי אפשר לעשות פונקציית יציאה לפונקציה, אבל ישנו פתרון אלטרנטיבי לבעיה. הפתרון פחות יפה וקשה יותר לתחזוקה מהפתרון ל-Action, ולכן אני ממליץ להשתמש בו רק בפונקציות מסובכות במיוחד, המכילות הרבה נקודות יציאה, וקוד יציאה "עבה". הפתרון מתבסס על פקודת Execute, ולכן אם היא לא מוכרת לכם, אני ממליץ לקרוא עליה בקבצי העזרה של ה-QTP. בשורה התחתונה, פקודת Execute מקבלת מחרוזת, ומבצעת את התוכן שלה כאילו שמדובר בפקודות VBScript. כך שהפקודה Execute "msgbox(2)" תקפיץ msgbox עם המספר 2. להלן דוגמה לשימוש בפתרון בפונקציה ComplexFunc:


Function ComplexFunc
   Dim sExitCode
   Dim sResult
   Dim oFile
'will be FSO textstream

  
'separate code lines by vbcrlf or ":"
  
sExitCode = "oFile.Close" &          vbcrlf & _
         "Set oFile = Nothing" & vbcrlf & _
         "ComplexFunc = sResult"
     
'More exit code

   'Function code goes here

   'Exit Gate
  
If CriticalCondition =
False Then
     
sResult = "False, No Connection"
      Execute sExitCode
      Exit Function
   End if

  
'More function code

   'Successful exit
  
Execute sExitCode
End Function