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 🙂
java.lang.String captureEntirePageScreenshotToString(java.lang.String kwargs)
java.lang.String captureScreenshotToString() |
java.lang.String captureEntirePageScreenshotToString(java.lang.String kwargs) java.lang.String captureScreenshotToString()
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:
import base64
...
def _absnorm(self, path):
return os.path.normpath(os.path.abspath(path.replace('/', os.sep)))
def _write_to_file(self, path, content, mode):
path = self._absnorm(path)
parent = os.path.dirname(path)
if not os.path.exists(parent):
os.makedirs(parent)
f = open(path, mode+'b')
f.write(content)
f.close()
return path
def capture_remote_screenshot(self, path=None):
"""Captures a screenshot and returns it as a string
Given path must be relative to Robot Framework output directory,
otherwise the embedded image is not shown in the log file. If path is
not given, file with name similar to 'selenium-image-x.png' is created
directly under the output directory.
"""
# configure path
if path and os.path.isabs(path):
raise RuntimeError("Given path must be relative to Robot outpudir")
if not path:
path = self._namegen.next()
outdir = NAMESPACES.current.variables['${outputdir}']
fullpath = os.path.join(outdir, path)
if not os.path.exists(os.path.split(fullpath)[0]):
os.makedirs(os.path.split(fullpath)[0])
# retrieve remote screenshot
self._info("Retrieving Screenshot")
screenshot = self._selenium.capture_entire_page_screenshot_to_string("background=#CCFFDD")
screenshot=base64.b64decode(screenshot)
# save screenshot
self._info("Saving screenshot to file '%s'" % fullpath)
self._write_to_file(fullpath, screenshot, 'w')
self._html('
<td></td>
</tr>
<tr>
<td colspan="3"><a href="http://blog.codecentric.de/2010/02/remote-screenshots-mit-selenium-und-dem-robot-framework/">' '</a></td>
</tr>
' % (path, path)) |
import base64 ... def _absnorm(self, path): return os.path.normpath(os.path.abspath(path.replace('/', os.sep))) def _write_to_file(self, path, content, mode): path = self._absnorm(path) parent = os.path.dirname(path) if not os.path.exists(parent): os.makedirs(parent) f = open(path, mode+'b') f.write(content) f.close() return path def capture_remote_screenshot(self, path=None): """Captures a screenshot and returns it as a string Given path must be relative to Robot Framework output directory, otherwise the embedded image is not shown in the log file. If path is not given, file with name similar to 'selenium-image-x.png' is created directly under the output directory. """ # configure path if path and os.path.isabs(path): raise RuntimeError("Given path must be relative to Robot outpudir") if not path: path = self._namegen.next() outdir = NAMESPACES.current.variables['${outputdir}'] fullpath = os.path.join(outdir, path) if not os.path.exists(os.path.split(fullpath)[0]): os.makedirs(os.path.split(fullpath)[0]) # retrieve remote screenshot self._info("Retrieving Screenshot") screenshot = self._selenium.capture_entire_page_screenshot_to_string("background=#CCFFDD") screenshot=base64.b64decode(screenshot) # save screenshot self._info("Saving screenshot to file '%s'" % fullpath) self._write_to_file(fullpath, screenshot, 'w') self._html(' <td></td> </tr> <tr> <td colspan="3"><a href="http://blog.codecentric.de/2010/02/remote-screenshots-mit-selenium-und-dem-robot-framework/">' '</a></td> </tr> ' % (path, path))
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
Setting | Value |
---|
Library | SeleniumLibrary | 10 | localhost | 4444 |
Library | OperatingSystem | | | |
| | | | |
Variable | Value |
---|
${BROWSER} | ff | | | |
| | | | |
Keyword | Action | Arguments |
---|
SeleniumTeardown | Run Keyword If Test Failed | Take 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 🙂
- SnapsIE 0.2 herunterladen und installieren
- 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:
Selenium.prototype.doCaptureEntirePageScreenshot = function(filename, kwargs) {
/**
* Saves the entire contents of the current window canvas to a PNG file.
* Contrast this with the captureScreenshot command, which captures the
* contents of the OS viewport (i.e. whatever is currently being displayed
* on the monitor), and is implemented in the RC only. Currently this only
* works in Firefox when running in chrome mode, and in IE non-HTA using
* the EXPERIMENTAL "Snapsie" utility. The Firefox implementation is mostly
* borrowed from the Screengrab! Firefox extension. Please see
* http://www.screengrab.org and http://snapsie.sourceforge.net/ for
* details.
*
* @param filename the path to the file to persist the screenshot as. No
* filename extension will be appended by default.
* Directories will not be created if they do not exist,
* and an exception will be thrown, possibly by native
* code.
* @param kwargs a kwargs string that modifies the way the screenshot
* is captured. Example: "background=#CCFFDD" .
* Currently valid options:
* <dl>
* <dt>background</dt>
* <dd>the background CSS for the HTML document. This
* may be useful to set for capturing screenshots of
* less-than-ideal layouts, for example where absolute
* positioning causes the calculation of the canvas
* dimension to fail and a black background is exposed
* (possibly obscuring black text).</dd>
* </dl>
*/
if (! browserVersion.isChrome &&
! (browserVersion.isIE && ! browserVersion.isHTA)) {
throw new SeleniumError('captureEntirePageScreenshot is only '
+ 'implemented for Firefox ("firefox" or "chrome", NOT '
+ '"firefoxproxy") and IE non-HTA ("iexploreproxy", NOT "iexplore" '
+ 'or "iehta"). The current browser isn\'t one of them!');
}
// do or do not ... there is no try
if (browserVersion.isIE) {
// targeting snapsIE >= 0.2
function getFailureMessage(exceptionMessage) {
var msg = 'Snapsie failed: ';
if (exceptionMessage) {
if (exceptionMessage ==
"Automation server can't create object") {
msg += 'Is it installed? Does it have permission to run '
+ 'as an add-on? See http://snapsie.sourceforge.net/';
}
else {
msg += exceptionMessage;
}
}
else {
msg += 'Undocumented error';
}
return msg;
}
if (typeof(runOptions) != 'undefined' &&
runOptions.isMultiWindowMode() == false) {
// framed mode
try {
Snapsie.saveSnapshot(filename, 'selenium_myiframe');
}
catch (e) {
throw new SeleniumError(getFailureMessage(e.message));
}
}
else {
// multi-window mode
if (!this.snapsieSrc) {
// XXX - cache snapsie, and capture the screenshot as a
// callback. Definitely a hack, because we may be late taking
// the first screenshot, but saves us from polluting other code
// for now. I wish there were an easier way to get at the
// contents of a referenced script!
if (/.hta/.exec(snapsieUrl))
{
snapsieUrl = "http://localhost:4444/selenium-server/Core/lib/snapsie.js";
}
var self = this;
new Ajax.Request(snapsieUrl, {
method: 'get'
, onSuccess: function(transport) {
self.snapsieSrc = transport.responseText;
self.doCaptureEntirePageScreenshot(filename, kwargs);
}
});
return;
}
// it's going into a string, so escape the backslashes
filename = filename.replace(/\\/g, '\\\\');
// this is sort of hackish. We insert a script into the document,
// and remove it before anyone notices.
var doc = selenium.browserbot.getDocument();
var script = doc.createElement('script');
var scriptContent = this.snapsieSrc
+ 'try {'
+ ' Snapsie.saveSnapshot("' + filename + '");'
+ '}'
+ 'catch (e) {'
+ ' document.getElementById("takeScreenshot").failure ='
+ ' e.message;'
+ '}';
script.id = 'takeScreenshot';
script.language = 'javascript';
script.text = scriptContent;
doc.body.appendChild(script);
script.parentNode.removeChild(script);
if (script.failure) {
throw new SeleniumError(getFailureMessage(script.failure));
}
}
return;
} |
Selenium.prototype.doCaptureEntirePageScreenshot = function(filename, kwargs) { /** * Saves the entire contents of the current window canvas to a PNG file. * Contrast this with the captureScreenshot command, which captures the * contents of the OS viewport (i.e. whatever is currently being displayed * on the monitor), and is implemented in the RC only. Currently this only * works in Firefox when running in chrome mode, and in IE non-HTA using * the EXPERIMENTAL "Snapsie" utility. The Firefox implementation is mostly * borrowed from the Screengrab! Firefox extension. Please see * http://www.screengrab.org and http://snapsie.sourceforge.net/ for * details. * * @param filename the path to the file to persist the screenshot as. No * filename extension will be appended by default. * Directories will not be created if they do not exist, * and an exception will be thrown, possibly by native * code. * @param kwargs a kwargs string that modifies the way the screenshot * is captured. Example: "background=#CCFFDD" . * Currently valid options: * <dl> * <dt>background</dt> * <dd>the background CSS for the HTML document. This * may be useful to set for capturing screenshots of * less-than-ideal layouts, for example where absolute * positioning causes the calculation of the canvas * dimension to fail and a black background is exposed * (possibly obscuring black text).</dd> * </dl> */ if (! browserVersion.isChrome && ! (browserVersion.isIE && ! browserVersion.isHTA)) { throw new SeleniumError('captureEntirePageScreenshot is only ' + 'implemented for Firefox ("firefox" or "chrome", NOT ' + '"firefoxproxy") and IE non-HTA ("iexploreproxy", NOT "iexplore" ' + 'or "iehta"). The current browser isn\'t one of them!'); } // do or do not ... there is no try if (browserVersion.isIE) { // targeting snapsIE >= 0.2 function getFailureMessage(exceptionMessage) { var msg = 'Snapsie failed: '; if (exceptionMessage) { if (exceptionMessage == "Automation server can't create object") { msg += 'Is it installed? Does it have permission to run ' + 'as an add-on? See http://snapsie.sourceforge.net/'; } else { msg += exceptionMessage; } } else { msg += 'Undocumented error'; } return msg; } if (typeof(runOptions) != 'undefined' && runOptions.isMultiWindowMode() == false) { // framed mode try { Snapsie.saveSnapshot(filename, 'selenium_myiframe'); } catch (e) { throw new SeleniumError(getFailureMessage(e.message)); } } else { // multi-window mode if (!this.snapsieSrc) { // XXX - cache snapsie, and capture the screenshot as a // callback. Definitely a hack, because we may be late taking // the first screenshot, but saves us from polluting other code // for now. I wish there were an easier way to get at the // contents of a referenced script! if (/.hta/.exec(snapsieUrl)) { snapsieUrl = "http://localhost:4444/selenium-server/Core/lib/snapsie.js"; } var self = this; new Ajax.Request(snapsieUrl, { method: 'get' , onSuccess: function(transport) { self.snapsieSrc = transport.responseText; self.doCaptureEntirePageScreenshot(filename, kwargs); } }); return; } // it's going into a string, so escape the backslashes filename = filename.replace(/\\/g, '\\\\'); // this is sort of hackish. We insert a script into the document, // and remove it before anyone notices. var doc = selenium.browserbot.getDocument(); var script = doc.createElement('script'); var scriptContent = this.snapsieSrc + 'try {' + ' Snapsie.saveSnapshot("' + filename + '");' + '}' + 'catch (e) {' + ' document.getElementById("takeScreenshot").failure =' + ' e.message;' + '}'; script.id = 'takeScreenshot'; script.language = 'javascript'; script.text = scriptContent; doc.body.appendChild(script); script.parentNode.removeChild(script); if (script.failure) { throw new SeleniumError(getFailureMessage(script.failure)); } } return; }