Testautomatisierung von Dropzone.js mit Selenium – Und was ich dabei lernte

Die wichtigen Dinge lernen wir nicht durch Schulen, wir lernen sie durch den offenen Blick, mit dem wir uns durch die Welt bewegen!

Ingrid Klaus Uschold

Testautomatisierung ist überall ein viel diskutiertes Thema. Bei der anforderungsgetriebenen Softwareentwicklung gilt es als nützliches Nebenprodukt. Dabei bevorzuge ich für die Akzeptanzkriterien die Dokumentation mit SpecFlow. Es entstehen dabei weitere, schöne Nebenprodukte. Zum einen wäre da die Nachvollziehbarkeit zum Backlog und zum anderen beim Einsatz des SpecFlow+ Runner, ein nützlicher Fortschrittsbericht, der je nach Kundenwunsch Printscreens oder Videos der Resultate enthalten kann.

Aufgabenstellung

Für ein paar User Stories gab es als Akzeptanzkriterium den Upload von Dateien. Beim Web-Frontend wurde dafür die Komponente Dropzone.js eingesetzt. Statt ein normales HTML-Eingabefeld für den Dateiupload gab es folgende Darstellung:

Nun ging es daran, einen Test dazu zu schreiben. Zuerst recherchierte ich im Internet, um zu sehen wie es andere lösten, denn ein einfaches Element.SendKeys in Selenium war mit dieser Darstellung nicht möglich. Ich hatte Glück, ich fand einen ausführlichen Blogbeitrag von Jacob Ferm, der einen Lösungsansatz beschrieb. Nachdem dieser implementiert war, ging es an den Test. Auf einer lokalen Umgebung mit Chrome funktionierte es. Als nächster Schritt war der Funktionalitätstest auf den Buildserver dran. Die Tests dort laufen mit Browserstack. Wunderbar, auch hier bestand der Test. Die Datei wurde korrekt hochgeladen das Verhalten war korrekt. Anforderung abgeschlossen, Definition of Done erfüllt.

Ein neues Szenario

Wie es so üblich ist, kam ein neues Szenario hinzu. Nachdem unser Kunde das maximale Limit pro Datei definiert hatte, musste der Test dafür implementiert werden. Das Problem, die implementierte Logik, funktionierte mit dem grösseren Test-File nicht. Die JavaScript-Logik erzeugte im Chrome-Driver einen Fehler mit der Meldung, dass der Web-Socket geschlossen wurde.

Ein Feedback, das ich in diesem Zusammenhang bekam, war: «Es ist schon sehr kompliziert implementiert, man muss ja nur den Pfeil klicken, dann öffnet sich der Dialog». Ich stellte mir die Frage: «Hat die Person schon mal einen File-Upload mit Selenium implementiert?» Selenium hat Einschränkungen, Dateien auf einen geöffneten Dialog zu setzen, funktioniert leider nicht.

Ich überlegte und recherchierte im Internet. Dabei fand ich auf github das Repository DialogCapabilities, welches mit geöffneten Dialogen umgehen kann. Ich war überrascht, auch von der Sachlichkeit des Autors. Ich schaute es mir an, es funktionierte mit P/Invoke. Kennst du das Attribute DllImport noch? Unmanged Code, lang ist es her, dachte ich mir. Wir verwenden eine .NET Standard Library, da funktionierte das doch nicht.

.NET Standard 2 und Unmanaged Code

Der Gedanke liess mich nicht los. Schnell ausprobieren und die Überraschung: P/Invoke mit .NET Standard 2 funktioniert.

Das war eine interessante Erkenntnis, wieder was gelernt.

Der Autor empfahl diesen Ansatz als Ausweg zu nutzen, da man mit dieser Logik am Ende an Windows gebunden und das Suchen des Dialogs sprachspezifisch sei.

Dropzone.js mit Selenium SendKeys

Ich entschied mich, den Code von dropzone.js zu analysieren. Wann und wo wird das HTML-Element vom Typ File hinzugefügt. Fündig wurde ich in der Datei dropzone.js ab Zeile 1199!!!!

In Zeile 1208 stand die wichtige Information zur CSS Klasse «dz-hidden-input».

Im NUnit-Test versuchte ich über die Auflösung des CSS-Klassennamen auf das Feld zuzugreifen und mit SendKeys den Dateipfad zu setzen. Es funktionierte, nun auch mit grösseren Dateien.

Seiteneffekte

Die BDD-Tests liefen lokal mit Chrome erfolgreich durch, ein Commit und auf Browserstack schlugen alle Szenarios mit Dateiupload-Funktionalität fehl. In der Fehlermeldung stand, dass die Datei nicht gefunden wurde. Ok, das Problem ist neu, woran liegts?

Browserstack verwendet den RemoteWebDriver. Hier benötigt es einen FileDetector, der die Datei vom lokalen Rechner in base64 umwandelt und anschliessend übermittelt. Mit dieser Ergänzung wurden die Dateien nun gefunden, die Tests waren wieder grün.

Zusammenfassung

Für den Upload von Dateien mit Dropzone.js fand ich drei Varianten:

Variante 1:

Die erste Lösung über JavaScript, die mit dem JavaScriptExecutor von Selenium ausgeführt wird. Ein Vorteil dieser Vorgehensweise: Sie braucht keinen FileDetector für den RemoteWebDriver.

public void AddFileToUploadQueue(string file, string formTagId)
{
    // Variante aus Blog 
    var mimeType = MimeMapping.MimeUtility.GetMimeMapping(file);
    var name = Path.GetFileName(file);
    Byte[] bytes = File.ReadAllBytes(file);
    var base64 = Convert.ToBase64String(bytes);

    var blobFunktion = @"function base64toBlob(b64Data, contentType, sliceSize) {  
            contentType = contentType || '';
            sliceSize = sliceSize || 512;

            var byteCharacters = atob(b64Data);
            var byteArrays = [];

            for (var offset = 0; offset < byteCharacters.length; offset += sliceSize) {
                var slice = byteCharacters.slice(offset, offset + sliceSize);

                var byteNumbers = new Array(slice.length);
                for (var i = 0; i < slice.length; i++) {
                    byteNumbers[i] = slice.charCodeAt(i);
                }

                var byteArray = new Uint8Array(byteNumbers);

                byteArrays.push(byteArray);
            }

            var blob = new Blob(byteArrays, {type: contentType});
            return blob;
        }";

    var script = blobFunktion + $@"
        var img = '{base64}';
        var blob = base64toBlob(img, '{mimeType}', 512);
        blob.name = '{name}';
        var myZone = Dropzone.forElement('#{formTagId}');
        myZone.addFile(blob);";

    WebDriver.ExecuteScript(script);
}

Variante 2:

Auch wenn ich mich über den Lösungsvorschlag wunderte, der Klick auf den Pfeil des Upload-Elements funktioniert, wenn man über Unmanaged Code den geöffneten Dateidialog sucht und über P/Invoke den Dateipfad setzt.

public void AddFileToUploadQueue(string file)
{
    var element = wd.FindElement(By.ClassName("dz-clickable"));
    element.Click();
    Dialogs.OpenFileDialog("Öffnen", file);
}

Variante 3:

Das versteckte HTML Element vom Typ File suchen und wie gewohnt den Dateipfad mit SendKeys setzen. Beim RemoteWebDriver muss jedoch der FileDetector vom Typ LocalFileDetector gesetzt werden, damit die Logik mit Browserstack funktioniert. Leider gibt es hier ein Datei-Limit von 30 MB.

public void AddFileToUploadQueue(string file, string formTagId)
{
    // Variante nach Analyse DropZone
    var upload = WebDriver.FindElement(By.ClassName("dz-hidden-input"));
    upload.SendKeys(file);
}
public IWebDriver SetupDriver()
{
    var driver = new RemoteWebDriver(
        new Uri("http://hub-cloud.browserstack.com/wd/hub/"),
        capabilities);

    // Für lokale Upload-Funktionalität von Dateien mit Browserstack notwendig.
    driver.FileDetector = new LocalFileDetector();

    return driver;
}

Die Überraschung:

.NET Standard akzeptiert Unmanaged Code Definitionen! Das P/Invoke mit dem bekannten Attribut DllImport funktioniert, dass hätte ich nicht erwartet.

Was für Erfahrungen hast du bei der Testautomatisierung gesammelt?

Dein Feedback interessiert mich!