בדיקות קבלה אוטומטיות לאתרי אינטרנט, בצד הלקוח.

Alonisser ,20/09/2012

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

פעם זה היה פשוט: אתר היה מורכב מדפים נפרדים, אפשר היה לבקש תכנותית לבקש דף, להוריד את הHtml שלו ולבדוק נוכחות של אלמנטים מיקומים וכו'. בפייטון זה היה נראה משהו כזה:

import requests #the best python web page handling library
from bs4 import BeautifulSoup
g = requests.get("http://haaretz.co.il")
assert g.status_code == 200
assert "הארץ" in g.text
#do all kind of beautifulsoup html parsing and checking here

כמובן שהיתה כאן בעיה: אני יכול לבדוק אם אלמנט נמצא, אבל אני לא יודע אם הדף מתנהג נכון, אם הכפתורים נלחצים אם הcss נכון מוצג וכו'. כן, אני יכול לנסות לעשות parsing לcss (ולנסות לדמות בעצמי עבודה של צוותי מפתחים ענקיים על דפדפנים ב15 שנה האחרונות), אבל בלי ספק זה פתרון מוגבל שמתמקד בתוכן הHtml ולא בפונקציואליות של הUI.

ויש בעיה גדולה יותר: אתרי אינטרנט מודרניים פשוט לא עובדים ככה, גם התוכן וגם הUI בנוי על המון js שבונה אותם דינאמית כך שfetch של הUrl פשוט לא יביא לנו תוצאה נכונה, שלא לדבר על אתרי Ajax שנבנו עם Js framework מודרנית, לא משנה אם מדובר בBackbone או אחרת. סביר שיש לנו רק דף אחד שהתוכן שלו משתנה דינאמית עם קריאות Ajax.  מה עושים? איך אפשר לבדוק אתרים כאלו מצד הUI.

שמתי את יהבי על עולם הקוד הפתוח וחיפשתי פתרון:

ראו הוזהרתם, אתם נכנסים לעולם של כאב.

ניסיון ראשון - הנחש מרים ראש

ניסיתי להשאר בעולם הפייטון שבו אני מרגיש נוח יותר ולאחר חיפוש די ארוך התמקדתי בשתי חלופות: Selenium - בעצם שרת java  מקומי + תוסף פיירפוקס שיש אליו bindings בשפות תכנות רבות. Selenium מאפשר לעשות אוטומטציה לדפדפן קיים. באופן מפתיע מריצים כמה שורות פקודה והדפדפן אשכרה נפתח ומתחיל לנוע בהתאם לפקודות. בדיקה עם סלניום נראית  משהו כזה (מטעמי עצלות העתקתי את הדוגמא מהאתר של סלניום בפייטון):

 

import unittest
from selenium import webdriver
from selenium.webdriver.common.keys import Keys
 
class PythonOrgSearch(unittest.TestCase):
 
    def setUp(self):
        self.driver = webdriver.Firefox()
 
    def test_search_in_python_org(self):
        driver = self.driver
        driver.get("http://www.python.org")
        self.assertIn("Python", driver.title)
        elem = driver.find_element_by_name("q")
        elem.send_keys("selenium")
        elem.send_keys(Keys.RETURN)
        self.assertIn("Google", driver.title)
 
    def tearDown(self):
        self.driver.close()
 
if __name__ == "__main__":
    unittest.main()

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

חוץ מזה, יש כאן שעטנז מסויים, כפי שהצביע בפני חבר, למה לא לבדוק את צד הלקוח באתר אינטרנט בשפה הטבעית שלו, בשפת הווב - ג'אווסקריפט. אז off we go  ל Js.

ניסיון שני - ללכת עם המתים

שאומרים Bdd בJs אומרים קודם כל Jasmine.  יש לה Html runner סימפטי וחברותי. הסינטקס שלה ברור ומסודר ויש יכולת פשוטה להרחיב את פונקציות הבדיקה. בקיצןר - כל מה שחיפשתי.

מהר מאוד הבנתי שjasmine יכולה להיות רק טכנולוגיית הרצת בדיקות, היא יודעת לבדוק פונקציית js, אבל בגלל שאני לא כותב הקוד, אני צריך להריץ את הדפים כדפדפן ומכאן שאני כלי ששייתכן לי  גישה תכנותית לדפדפן בJs שjasmine יודעת להריץ. בעבר כתבתי כאן על אלטרנטיבות כאלו של headless browsers ובגלל שהדוגמאות של PhantomJs היו מוגבלות והapi מורכב יחסית החלטתי לבחור  ב Zombie.js של אסף ארקין.

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

בדרך:

קודם כל הייתי צריך להתקין (לצערי, אני עובד על חלונות מה שמסבך הכל) Node.js, יש גרסה להתקנה פשוטה על חלונות ישירות מהאתר (בניגוד לטענה של חלק מהמדריכים) וNpm כמנהל חבילות הוא מוצלח למדי - התקנתי את zombie כך:

Mkdir zombiedir
cd zombiedir
npm install zombie

בעיות:

כיוון שחלק מהתלויות (Dependencies)  של Zombie.js דורשות קמפול מ c או c++ (ספציפית בעייתית כאן תלויה של תלויה - contextify של JSDOM) הייתי צריך להתקין גם visual studio express 2010(כיון שאני לא פאנבוי של סביבות הc# ו.net אין לי כזה) בשביל הc compiler.

מכאן ביליתי יום בלא להבין למה ההתקנה עדיין נכשלת בגלל בעיית קמפול שהתבררה כאי תאימות בין הקמפול שנותנת גרסת הexpress ל32 ביט, לגרסת הNode.js של 64 ביט שהייתה ברירת המחדל של המחשב שלי.הסרתי את Node התקנתי מחדש עם גרסת 32 ביט, ועכשיו ההתקנה עבדה. לתשומת ליבכם שההתקנה גם דורשת פייטון, אבל סביר להניח שאם אתם קוראים את הבלוג הזה יש לכם כבר פייטון מותקנת. אפשר לגשת לעבודה.

רגע! מסתבר, שבניגוד לחלק מההספריות של Node.js אין דרך לקרוא לzombie.js בסביבת דפדפן ועל כן הייתי צריך להשתמש בjasmine-node - מודול jasmine מותאם להרצה בnode. שאותו יש להתקין כך:

npm -g install jasmine-node

שהפרמטר -g נחוץ כדי שjasmine-node תותקן גלובלית ותתאפשר הרצה שלה משורת הפקודה. חלק מהמשמעות של ההחלפה היתה לוותר על הHtml runner היפה של jasmine, אם כי אני מניח שעם קצת נבירה הייתי מצליח לחבר ביניהם (ומקסימום בונה אפליקציית Node express קטנה שתעשה את זה).

כמו כן כדאי לשים לב שjasmine יודעת להריץ רק קבצים שהשם שלהם מסתיים בSpec.js מה שגרם לי להרבה תסכול ואי הבנה מדוע הקבצים לא רצים עד שהבנתי את הטעות. ומכאן שלקובץ הבדיקות שלנו צריך לקרוא כך:

jasmine-node zombieSpec.js

להחיות את המתים:

אז איך נראה קובץ ZombieSpec.js כזה לדוגמא? (מעצלות העתקתי מהמדריך הרשמי והוספתי ביאורים בעברית):

var Browser = require("zombie");
var assert = require("assert");
 
// Load the page from localhost
browser = new Browser()
browser.visit("http://localhost:3000/", function () { //פונקציית קולבק אסיכרונית שנטענת אחרי העליה לדף
 
  // Fill email, password and submit form
  browser.
    fill("email", "[email protected]").//הנקודות משרשרות להמשך על אותו אובייקט דפדפן
    fill("password", "eat-the-living").
    pressButton("Sign Me Up!", function() { //שוב פונקציית קולבק שאמורה לרוץ רק אחרי שכל הקוד נטען
 
      // Form submitted, new page loaded.
      assert.ok(browser.success);
      assert.equal(browser.text("title"), "Welcome To Brains Depot");
 
    })
 
});
 

שהמבנה כמובן צריך להיות מותאם יותר לjasmine עם סינטקס הBdd שלה שכולל Describe, it, expect וכו'.  הApi של zombie רחב וכולל המון אפשרויות לצעידה בתוך דף אינטרנט, הselector של אלמנטים בדף מבוסס על sizzle הפופלארי (המנוע של בחירת האלמנטים בjQuery) מה שהופך את כל החוויה לידידותית למפתח ממוצע כמוני והיא מובנה לשילוב עם מגוון ספריות בדיקה מעבר לjasmine כמו: Mocha (שנראה שzombie נבנתה מראש לשילוב איתה). הנה דוגמא מהאתר לבדיקה אסינכרונית מבוססת Mocha וסינטקס Bdd:

describe("visit", function() {
  before(function(done) { //לחילופין אפשר גם לקרוא ל before each
    this.browser = new Browser();
    this.browser
      .visit("/promises")
      .then(done, done); //סוף הקריאה האסינכרונית
  });
 
  it("should load the promises page", function() {
    assert.equal(this.browser.location.pathname, "/promises");
  });
});

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

מודה שיכול להיות שנכשלתי כי צד הJs האסינכרוני שלי חלש יחסית, כי אני לא משתמש Node וותיק או כי אני לא מבין נכון את הApi של Jasmine-node או.. לא יודע אולי סתם גרסה לא מדוייקת של זומבי.

אבל מה שבסופו של דבר שבר אותי הוא חולשתה היחסית של קהילת המשתמשים והמפתחים (אם כי הפרוייקט בפיתוח אינטנסיבי למדי). חלק מהמחירים של קוד פתוח. כנראה שבזירה שיש בה הרבה מתמודדים ומבוססת על טכנולוגייות מהקצה (Node.js) קשה למצוא מענה ותמיכה. שאלות שלי בGoogle group זכו להתעלמות ו/או לתהייה משותפת ו/או לתשובת שורה אחת מהמפתח: "לא".  שאלה מפורטת שלי בStackoverflow לא זכתה אפילו לתשובה אחת, נדיר למדי מבחינת החוויה שלי שם. אז עברתי הלאה, ולהבדיל מהפעם הראשונה, הפעם מצאתי ספרייה שמלבישה רובד שימושי יותר על Phantomjs . מצאתי את casper.js ועברתי אליה.

מבחינתי לפחות לא הייתי מספיד את זומבי, היא צעירה מאוד אבל נראה שהיא בדרך למאסה קריטית, ובפירוש אנסה לחזור אליה בהמשך, בטח באתר שאפתח מאפס.

ניסיון שלישי (ועובד!) - רוח רפאים ידידותית

 Casper.js היא רובד הפשטה שיושב מעל  המנוע של Phantomjs, הפנטום בפני עצמו הוא מנוע דפדפן ללא תצוגה למשתמש (Headless browser) שמבוסס על המנוע של Webkit, בגלל שNode.js מבוססת על מנוע הV8 של google בעוד שפנטום משתמש (כנראה) בJavascriptcore של וובקיט, הפלטפורמות לא תואמות. מה שאומר שלפחות כרגע (להבנתי) אי אפשר להריץ את casper.js בסביבת Node.js.  המטרה של קספר היא כאמור להוסיף פונקציונאליות נוחה יותר לסקריפטינג של הדפדפן, עם הרבה syntactic sugar על הApi של הפאנטום ורובד של בדיקות - הי, בדיוק מה שחיפשנו.

גם כאן, ההתקנה על חלונות היא לא כיפית (מה כן כיפי בחלונות בעולם התוכנה? ואל תגידו .net כי שום דבר לא כיפי בזה), דורשת פייטון מותקן, Phantomjs שהורדתם והוספתם לpath,  הורדת git clone של casper וכמה שורות נוספות בראש הקובץ. במפתיע הוראות ההתקנה לחלונות באתר שלהם עובדות ממש.

ככה נראה סקריפט בדיקה של casper.js (מטעמי עצלות, בעיקר העתקתי והוספתי ביאורים בעברית):

//אם אתם משתמשי חלונות כאן צריך להוסיף את שתי השורות הראשונות שבהוראות ההתקנה
var casper = require('casper').create();
 
casper.start('http://www.google.fr/', function() {
    this.test.assertTitle('Google', 'google homepage title is the one expected');
    this.test.assertExists('form[action="/search"]', 'main form is found');
    this.fill('form[action="/search"]', {
        q: 'foo'
    }, true);
});
 
casper.then(function() { // נקרא ליניארית שהפונקציה הקודמת סיימה, לי לפחות זה פתר את כל בעיות האסינכרוניות, זה פשוט עובד
    this.test.assertTitle('foo - Recherche Google', 'google title is ok');
    this.test.assertUrlMatch(/q=foo/, 'search term has been submitted');
    this.test.assertEval(function() {// פונקצית בדיקה שמריצה קוד באתר שאתם בודקים, לא אצלכם, אל תתבלבלו בזה
        return __utils__.findAll('h3.r').length >= 10;
    }, 'google search for "foo" retrieves 10 or more results');
});
 
casper.run(function() { //הרצת חבילת הבדיקות
    this.test.renderResults(true);
});

וקיבלתי בconsole שורה של בדיקות עם pass או fail פשוט, מהיר וברור.
יש עוד יתרונות לcasper - אפשרות לדחוף ספריות Js או סקריפטים (נאמר Jquery לפשט לכם את העבודה אם יש אתר שלא משתמש) לאתר שאתם בודקים , וכאמור api נחמד ורחב יחסית ומותאם בפשטות לעבודה שאני רוצה לבצע (למשל לבדוק האם אלמנט הוא visible ולא רק אם הוא נוכח).
כמובן שיש גם מינוסים,:אם יש לכם טעות בסינטקס של casper פשוט תקבלו parse error מיסתורי  - ללא רמז לאיזו שורה, מה סוג הבעיה או בכלל.. מקשה לדבג חבילות בדיקה גדולות ולהבין מה הסיפור. מינוס מרכזי בעולם הJs היא שקספר, לא  בNode אז אתם לא מקבלים את האקוספריה הגדולה שלה. כמו כן נראה שלהבדיל מjasmine כמות הreporters שלה מוגבלת בהרבה, כך שלהפוך את הפלט שלה לדף ווב יפה יקח קצת יותר עבודה (אבל בטח לא משהו בלתי אפשרי, קספר יודעת להוציא  בפורמט xml מסויים שלמרות שאני שונא xml אפשר לעבד אותו), אין את מנוע הסלקטורים החכם שזומבי עובדת עליו (אבל עם קוד קצת יותר מפורט והשתלת Jquery תוכלו לעשות כזה בעצמכם).

מה עוד אני לא יודע? אני לא יודע איך והאם הפתרונות שסקרתי בו יודעים לגדול (scale) , להתמודד עם קלט משתנים וכו'

למדתי הרבה בהרפתקאה הזו ומקווה שגם אתם למדתם משהו חדש. ביי בינתיים

מוזמנים להעיר/לשאול

תגובה אחת

  1. [...] ספרייה בדיקות שימושית שיושבת מעל Phantomjs ו CasperJs (שעליהן כתבנו בהרחבה בעבר). היא מאפשרת להשוות את התוצאה של רנדור הcss של אתר [...]

תגובה