//

Remote Screenshots mit Selenium und dem Robot Framework

10.2.2010 | 8 Minuten Lesezeit

Screenshots sind bei Oberflächentests sehr hilfreich, um schnell visuelles Feedback zu bekommen, warum ein Test fehlgeschlagen ist. Selenium bietet hierzu einige gute Möglichkeiten. Diese stoßen Momentan allerdings an viele Grenzen, von denen einige hier im Blogpost diskutiert und sogar gelöst werden sollen

Achtung (2010-02-15): Dieser Artikel bezieht sich auf die SeleniumLibrary 2.2. Die weiter unten vorgeschlagene Änderung hat mittlerweile Eingang in die SeleniumLibrary 2.3 gefunden (Release Notes ). Vielen Dank an das Robot Framework Team!

Unsere bevorzugte Art und Weise, Selenium in die automatischen Fachtests zu integrieren ist das Robot Framework. Dadurch ist es möglich Tests über alle Schichten zu fahren, von der Datenbank bis hin zur UI. Und das sogar täglich (bzw. nächtlich) oder bei jedem commit. Hier begegnet uns auch schon das erste Problem. Unser CI-Server (Hudson) läuft unter Linux … und ohne XServer. D.h. der Selenium Server kann nicht auf dem Server laufen, von dem aus die automatischen Fachtests angestoßen werden. Ein weiterer Grund, den Selenium-Server woanders laufen zu lassen, ist auch dass es schwierig wird, unter Linux den Internet Explorer zum testen zu benutzen; trotz aller Sicherheitslücken und Wechselempfehlungen ein in Intranets immer noch ein häufig genutzter Browser.

Viele Screenshot-Funktionen des Selenium-Servers nehmen als Argument die Datei, in welche der Screenshot gespeichert werden soll; ungünstig, wenn man dann nur wieder über Umwege (scp, fileshares, …) an die Screenshots kommt. Gerade im Fehlerfall sollte die Analyse ja so einfach, wie möglich sein. Es gibt allerdings auch zwei Funktionen in Selenium , die den Screenshot als String (base64 encodiertes PNG) an den Client zurückgeben. Das scheint doch ein guter Hebel zu sein 🙂

1java.lang.String captureEntirePageScreenshotToString(java.lang.String kwargs)
2java.lang.String captureScreenshotToString()
3

Die bestehende SeleniumLibrary für das Robot Framework, stellt allerdings leider die beiden Selenium-Server-Methoden nicht als Keyword bereit, hier heißt es also noch manuell Hand anlegen, bis das offiziell supported wird (Issue 89 ). Dazu lädt man sich die aktuelle Source-Distribution der Robot SeleniumLibrary 2.2.2 herunter, und editiert die Datei __init__.py. Man verzeihe mir hoffentlich meine stümperhaften Python-Gehversuche:

1import base64 
2 
3...
4 
5    def _absnorm(self, path):
6        return os.path.normpath(os.path.abspath(path.replace('/', os.sep)))
7 
8    def _write_to_file(self, path, content, mode):
9        path = self._absnorm(path)
10        parent = os.path.dirname(path)
11        if not os.path.exists(parent):
12            os.makedirs(parent)
13        f = open(path, mode+'b')
14        f.write(content)
15        f.close()
16        return path
17 
18    def capture_remote_screenshot(self, path=None):
19        """Captures a screenshot and returns it as a string
20 
21        Given path must be relative to Robot Framework output directory,
22        otherwise the embedded image is not shown in the log file. If path is
23        not given, file with name similar to 'selenium-image-x.png' is created
24        directly under the output directory.
25        """
26 
27        # configure path
28        if path and os.path.isabs(path):
29            raise RuntimeError("Given path must be relative to Robot outpudir")
30        if not path:
31            path = self._namegen.next()
32        outdir = NAMESPACES.current.variables['${outputdir}']
33        fullpath = os.path.join(outdir, path)
34        if not os.path.exists(os.path.split(fullpath)[0]):
35            os.makedirs(os.path.split(fullpath)[0])
36 
37        # retrieve remote screenshot
38        self._info("Retrieving Screenshot")
39        screenshot = self._selenium.capture_entire_page_screenshot_to_string("background=#CCFFDD")
40        screenshot=base64.b64decode(screenshot)
41 
42        # save screenshot
43        self._info("Saving screenshot to file '%s'" % fullpath)
44        self._write_to_file(fullpath, screenshot, 'w')
45        self._html('
46<td></td>
47</tr>
48<tr>
49<td colspan="3"><a href="http://blog.codecentric.de/2010/02/remote-screenshots-mit-selenium-und-dem-robot-framework/">'                   '</a></td>
50</tr>
51' % (path, path))
52

Die Funktionen _absnorm und _write_to_file habe ich mir aus der OperatingSystem Robot Library geborgt (und etwas modifiziert). Um die modifizierte Selenium Library zu installieren reicht ein aufruf von „setup.py install“ auf.

Wie kann man nun das neue Keyword am besten in seinen Robot Testcases benutzen? Ich finde es sehr hilfreich bei einem fehlschlagenden Test einen Screenshot vom Browser zu machen, um schnell sehen zu können, was das Problem ist. Dazu kann das Teardown des Testcases genutzt werden, das im Falle eines Fehlschlags das neue Keyword aufruft:

Testing

SettingValue
LibrarySeleniumLibrary10localhost4444
LibraryOperatingSystem
VariableValue
${BROWSER}ff
Test CaseActionArguments
TestSelenium PASS Open Browserhttp://www.google.de ${BROWSER}
[Teardown]Selenium Teardown
TestSelenium FAIL Open Browserhttp://www.google.de ${BROWSER}
Page Should ContainAAAAAAAAAAAAA-IMPOSSIBLETEXT
[Teardown]Selenium Teardown
KeywordActionArguments
SeleniumTeardown Run Keyword If Test FailedTake Screenshot
Close Browser
Take Screenshot Run Keyword If‚${BROWSER}‘ != ‚*iexplore‘ and ‚${BROWSER}‘ != *’ie‘ and ‚${BROWSER}‘ != ‚*internetexplorer‘ and ‚${BROWSER}‘ != ‚*iehta‘Capture Remote Screenshot

So wird in jedem Teardown geprüft ob der Test fehlgeschlagen ist. Ist dies der Fall wird das Keyword „Take Screenshot“ aufgerufen. Diesen prüft noch den Browser, und nur wenn dieser nicht der Internet Explorer ist, wird das neue Keyword „Capture Remote Screenshot“ aufgerufen, welchen den Screenshot dann auch sofort ins Logfile einbettet:

Um die Screenshots auch noch mit dem IE hinzubekommen waren ein paar Klimmzüge notwendig. Zuerst muss man feststellen, dass man die richtige Methode in selenium aufrufen muss, ansonsten bekommt man nämlich nur einen komplett schwarzen Screenshot. Benutzt man die Methode selenium.capture_screenshot_to_string() bleibt der Screenshot schwarz. Diese Methode benutzt Java um vom kompletten Bildschirm einen Screenshot zu machen (also nicht nur vom Browserinhalt). Das funktioniert zwar auch mit dem Internet Explorer, allerdings überhaupt nicht, wenn der Selenium Server in Hintergrund läuft. Bei uns läuft der Selenium Server auf einem Remote Desktop, und um von ganzen Bildschirm nicht-schwarze Screenshots zu bekommen muss man eingeloggt sein – unpraktisch. Eine Alternative war ein Setup über VNC, was uns aber nicht weniger unpraktisch erschien (von stackoverflow ):

What we do is launch everything from under the context of a VNC session. On Windows, configure VNC to launch a session upon startup. Then make sure the user auto-logs in. Then place a .bat file in Program Files->Startup that launches Selenium RC. It’s kind of a pain, but it’s the most reliable way I’ve found for ensuring that Selenium RC starts in an environment that supports screenshots, launching IE, interacting with native events, etc.

Die Alternative ist, nicht den gesamten Bildschirm zu fotografieren, sondern nur den Browserinhalt. Dies hat auch den Vorteil, dass man die komplette Webseite sieht, wenn sie größer als der Bildschirm ist 😉 Diese Methode benutzt Javascript um an den gerenderten Browserinhalt zu kommen. Nun das Problem: Funktioniert wunderbar mit dem Firefox, aber überhaupt nicht mit dem Internet Explorer. Noch nicht. Hier ist was zu tun ist — so dachte ich. Das folgende hat sich als Sackgasse entpuppt, falls da jemand mehr Erfolg hat, als ich wäre ich sehr daran interessiert, wie.

WIN: Screenshots mit dem IE

Die für mich einzige funktionierende Methode, um auch mit dem IE Screenshots zu machen, ist den Selenium Server im „singleWindow“ Modus laufen zu lassen. Dazu startet man diesen folgendermaßen:

java -jar selenium-server.jar -singleWindow

Zudem muss Selenium als Browserprofil „*iexploreproxy“ benutzen, deshalb filtert der Testcase oben auch alle anderen IE-Profile aus.

Achtung: Vielleicht muss man trotzdem die neueste Snapsie-Bibliothek installieren (siehe unten). Ob das funktioniert, wenn ich diese wieder deinstalliere habe ich jetzt nicht getestet.

Achtung (2010-02-15): SnapsIE 0.2 muss auf jeden Fall installiert werden, damit Screenshots auch mit dem IE funktionieren!

FAIL: Screenshots mit dem IE

Das hier hat leider nicht funktioniert. Wie gesagt, wenn jemand weiß warum, bitte melden 🙂

  1. SnapsIE 0.2 herunterladen und installieren
  2. Das selenium-server-1.0.1.jar aus packen und
    • das core\lib\snapsie.js mit der version von SnapsIE 0.2 ersetzen
    • Die Datei core\scripts\selenium-api.js nach dem Vorschlag von Elf aus dem Selenium-Forum abändern. Ich hab die Änderung noch etwas erweitert, deshalb hier die kompette Methode:
1Selenium.prototype.doCaptureEntirePageScreenshot = function(filename, kwargs) {
2    /**
3     * Saves the entire contents of the current window canvas to a PNG file.
4     * Contrast this with the captureScreenshot command, which captures the
5     * contents of the OS viewport (i.e. whatever is currently being displayed
6     * on the monitor), and is implemented in the RC only. Currently this only
7     * works in Firefox when running in chrome mode, and in IE non-HTA using
8     * the EXPERIMENTAL "Snapsie" utility. The Firefox implementation is mostly
9     * borrowed from the Screengrab! Firefox extension. Please see
10     * http://www.screengrab.org and http://snapsie.sourceforge.net/ for
11     * details.
12     *
13     * @param filename  the path to the file to persist the screenshot as. No
14     *                  filename extension will be appended by default.
15     *                  Directories will not be created if they do not exist,  
16     *                  and an exception will be thrown, possibly by native
17     *                  code.
18     * @param kwargs    a kwargs string that modifies the way the screenshot
19     *                  is captured. Example: "background=#CCFFDD" .
20     *                  Currently valid options:
21     *                  <dl>
22     *                   <dt>background</dt>
23     *                     <dd>the background CSS for the HTML document. This
24     *                     may be useful to set for capturing screenshots of
25     *                     less-than-ideal layouts, for example where absolute
26     *                     positioning causes the calculation of the canvas
27     *                     dimension to fail and a black background is exposed
28     *                     (possibly obscuring black text).</dd>
29     *                  </dl>
30     */
31    if (! browserVersion.isChrome &&
32        ! (browserVersion.isIE && ! browserVersion.isHTA)) {
33        throw new SeleniumError('captureEntirePageScreenshot is only '
34            + 'implemented for Firefox ("firefox" or "chrome", NOT '
35            + '"firefoxproxy") and IE non-HTA ("iexploreproxy", NOT "iexplore" '
36            + 'or "iehta"). The current browser isn\'t one of them!');
37    }
38    // do or do not ... there is no try
39 
40    if (browserVersion.isIE) {
41        // targeting snapsIE >= 0.2
42        function getFailureMessage(exceptionMessage) {
43            var msg = 'Snapsie failed: ';
44            if (exceptionMessage) {
45                if (exceptionMessage ==
46                    "Automation server can't create object") {
47                    msg += 'Is it installed? Does it have permission to run '
48                        + 'as an add-on? See http://snapsie.sourceforge.net/';
49                }
50                else {
51                    msg += exceptionMessage;
52                }
53            }
54            else {
55                msg += 'Undocumented error';
56            }
57            return msg;
58        }
59 
60        if (typeof(runOptions) != 'undefined' &&
61            runOptions.isMultiWindowMode() == false) {
62            // framed mode
63            try {
64                Snapsie.saveSnapshot(filename, 'selenium_myiframe');
65            }
66            catch (e) {
67                throw new SeleniumError(getFailureMessage(e.message));
68            }
69        }
70        else {
71            // multi-window mode
72            if (!this.snapsieSrc) {
73                // XXX - cache snapsie, and capture the screenshot as a
74                // callback. Definitely a hack, because we may be late taking
75                // the first screenshot, but saves us from polluting other code
76                // for now. I wish there were an easier way to get at the
77                // contents of a referenced script!
78                if (/.hta/.exec(snapsieUrl))
79                {
80                    snapsieUrl = "http://localhost:4444/selenium-server/Core/lib/snapsie.js";
81                }
82                var self = this;
83                new Ajax.Request(snapsieUrl, {
84                    method: 'get'
85                    , onSuccess: function(transport) {
86                        self.snapsieSrc = transport.responseText;
87                        self.doCaptureEntirePageScreenshot(filename, kwargs);
88                    }
89                });
90                return;
91            }
92 
93            // it's going into a string, so escape the backslashes
94            filename = filename.replace(/\\/g, '\\\\');
95 
96            // this is sort of hackish. We insert a script into the document,
97            // and remove it before anyone notices.
98            var doc = selenium.browserbot.getDocument();
99            var script = doc.createElement('script'); 
100            var scriptContent = this.snapsieSrc
101                + 'try {'
102                + '    Snapsie.saveSnapshot("' + filename + '");'
103                + '}'
104                + 'catch (e) {'
105                + '    document.getElementById("takeScreenshot").failure ='
106                + '        e.message;'
107                + '}';
108            script.id = 'takeScreenshot';
109            script.language = 'javascript';
110            script.text = scriptContent;
111            doc.body.appendChild(script);
112            script.parentNode.removeChild(script);
113            if (script.failure) {
114                throw new SeleniumError(getFailureMessage(script.failure));
115            }
116        }
117        return;
118    }
119

Beitrag teilen

Gefällt mir

0

//

Weitere Artikel in diesem Themenbereich

Entdecke spannende weiterführende Themen und lass dich von der codecentric Welt inspirieren.

//

Gemeinsam bessere Projekte umsetzen

Wir helfen Deinem Unternehmen

Du stehst vor einer großen IT-Herausforderung? Wir sorgen für eine maßgeschneiderte Unterstützung. Informiere dich jetzt.

Hilf uns, noch besser zu werden.

Wir sind immer auf der Suche nach neuen Talenten. Auch für dich ist die passende Stelle dabei.