Variante: Automatisierte GUI-Steuerung • bisherige Ansätze auf Java eingeschränkt • generell: Capture & Replay gibt es, oft programmiersprachenabhängig, in vielen Varianten • generell: Kosten und Nutzen bzgl. erwarteter Änderungen überlegen • Variante: Software steuert die Bedienung anhand von Bildern • Idee: Bildausschnitte werden zuerst fotografiert, dann bei Ausführung gesucht; Bildausschnitte können dann geklickt und über Tastatur gefüllt werden • Testen: Prüfung, ob erwartete Bildausschnitte gefunden werden • hier: Sikuli-API (https://code.google.com/p/sikuli-api/) Software-Qualität Stephan Kleuker 274 Testrechner • Nutzung hängt stark vom Testrechner ab (Geschwindigkeit, Auflösung, gewähltes GUI-Design, Anzahl angeschlossener Monitore) • Ansatz: es gibt feste Testrechner, die zum Testen genutzt werden, mit klarer Konfiguration • sinnvolle Variante: Nutzung virtueller Rechner • keine durchsichtigen Fenster oder Ränder • Prozessautomatisierung zur Testausführung (aufspielen, ausführen, Ergebnis einsammeln) Software-Qualität Stephan Kleuker 275 Hilfsmittel • Screencapturer, z. B. FastStone Capture • Bildanalyseprogramm mit Pixelangaben, z. B. Photofiltre • ausgehend von links-oben muss relativ (67,11) geklickt werden http://www.portablefreeware.com/?id=775 http://portableapps.com/apps/graphics_pictures/photofiltre_portable Software-Qualität Stephan Kleuker 276 Genereller Ansatz ScreenRegion s = new DesktopScreenRegion(); DesktopScreenRegion(); File file = new File(" File("bilder bilder/zonen.png"); bilder/zonen.png"); Target target = new ImageTarget( ImageTarget(file); file); Bildschirm aufnehmen zu suchendes Bild wird zum Ziel Mindestgenauigkeit mit der Bild gefunden werden muss target.setMinScore(0.9 target.setMinScore(0.9); (0.9); ScreenRegion r = s.find( s.find(target); target); Mouse mouse = new DesktopMouse(); DesktopMouse(); mouse.click( mouse.click(r.getCenter()); r.getCenter()); finde passenden Bildausschnitt mit höchster Genauigkeit; null wenn nicht gefunden erzeuge Maus zur Steuerung, mache einfachen Links-klick Keyboard keyboard = new DesktopKeyboard DesktopKeyboard(); (); keyboard.type("33"); keyboard.type("33"); erzeuge Tastatur und gebe an aktueller Position den Text ein Software-Qualität Stephan Kleuker 277 wichtige Varianten List<ScreenRegion List<ScreenRegion> ScreenRegion> sr = s.findall( s.findall(target); target); findet alle Regionen, in denen eine Mindestübereinstimmung mit dem target gefunden wird, absteigend geordnet ScreenRegion r = s.find( s.find(target); target); rec = r.getBounds(); r.getBounds(); rec.x = 75; rec.y = 13; rec.height = 0; rec.width = 0; r.setBounds( r.setBounds(rec); rec); mouse.click( mouse.click(r.getCenter()); r.getCenter()); ermöglicht die genaue Festlegung des Punktes, auf den geklickt werden soll Software-Qualität Stephan Kleuker 278 Ausschnitt: wichtige Klassen (1/2) Software-Qualität Stephan Kleuker 279 Ausschnitt: wichtige Klassen (2/2) Software-Qualität Stephan Kleuker 280 Hilfsmittel zur Visualisierung ScreenRegion s = new DesktopScreenRegion(); DesktopScreenRegion(); Target target = new ImageTarget( ImageTarget(new File("bilder File("bilder/Uhrzeit.png bilder/Uhrzeit.png")); /Uhrzeit.png")); ScreenRegion r = s.find( s.find(target); target); Canvas canvas = new DesktopCanvas(); DesktopCanvas(); // Zeichenflaeche canvas.addBox(r canvas.addBox(r); (r); canvas.addLabel(r canvas.addLabel(r, (r, r.getBounds(). r.getBounds().toString ().toString()); toString()); canvas.display(3); canvas.display(3); // Sekunden Software-Qualität Stephan Kleuker 281 Hilfsprogramm zur Analyse (1/3) import import import import import java.io.File; java.io.File; java.nio.file.DirectoryStream; java.nio.file.DirectoryStream; java.nio.file.Files; java.nio.file.Files; java.nio.file.Path; java.nio.file.Path; java.nio.file.Paths; java.nio.file.Paths; import import import import import import org.sikuli.api.DesktopScreenRegion; org.sikuli.api.DesktopScreenRegion; org.sikuli.api.ImageTarget; org.sikuli.api.ImageTarget; org.sikuli.api.ScreenRegion; org.sikuli.api.ScreenRegion; org.sikuli.api.Target; org.sikuli.api.Target; org.sikuli.api.visual.Canvas; org.sikuli.api.visual.Canvas; org.sikuli.api.visual.DesktopCanvas; org.sikuli.api.visual.DesktopCanvas; public class Analyse { private final String ORDNER ="bilder/"; Software-Qualität Stephan Kleuker 282 Hilfsprogramm zur Analyse (2/3) public void zeigeDetails(String zeigeDetails(String dateiname){ dateiname){ System.out.println("suche: System.out.println("suche: " + dateiname); dateiname); ScreenRegion s = new DesktopScreenRegion(); DesktopScreenRegion(); //s //s = new DesktopScreenRegion( DesktopScreenRegion(-2560,0,700,1000); Target target = new ImageTarget( ImageTarget(new File(dateiname File(dateiname)); dateiname)); target.setMinScore(0.7); target.setMinScore(0.7); Canvas canvas = new DesktopCanvas(); DesktopCanvas(); int anzahl = 0; for (ScreenRegion sc : s.findAll( s.findAll(target)) target)) { canvas.addBox( canvas.addBox(sc); sc); canvas.addLabel( canvas.addLabel(sc, sc, dateiname+": dateiname+": "+sc.getScore "+sc.getScore()); sc.getScore()); anzahl++; anzahl++; } if( if(anzahl > 0){ canvas.display(8); canvas.display(8); } else { System.out.println("Bild System.out.println("Bild nicht gefunden"); } Software-Qualität Stephan Kleuker } 283 Hilfsprogramm zur Analyse (3/3) public void alle(){ Path dir = Paths.get(ORDNER); Paths.get(ORDNER); try (DirectoryStream (DirectoryStream<Path> DirectoryStream<Path> stream = Files .newDirectoryStream( newDirectoryStream(dir, dir, "*.png "*.png")) png")) { for (Path entry : stream) stream) { zeigeDetails( zeigeDetails(entry.toString()); entry.toString()); } } catch (Exception (Exception e) { System.out.println(e); System.out.println(e); } } public void eines(String name){ name){ zeigeDetails(ORDNER zeigeDetails(ORDNER + name +".png +".png"); png"); } public static void main(String[] args) args) { new Analyse().alle(); } } Software-Qualität Stephan Kleuker 284 Schwellwertanalyse (1/3) auch genau passende Ausschnitte liefern nicht unbedingt 1.0 Software-Qualität Stephan Kleuker 285 Schwellwertanalyse (2/3) auch unpassende Ausschnitte liefern evtl. hohe Werte Software-Qualität Stephan Kleuker 286 Schwellwertanalyse (3/3) generell möglichst große, sich verändernde Flächen (hier recht klein) Software-Qualität Stephan Kleuker 287 Sikuli-Nutzung (1/10): Testarchitektur Software-Qualität Stephan Kleuker 288 Sikuli-Nutzung (2/10): benutzte Bilder • Steuerung • Ergebnisüberprüfung Software-Qualität Stephan Kleuker 289 Sikuli-Nutzung (3/10): zentrale Variablen public class GUIBedienung { private private private private private String verzeichnis = "bilder "bilder/"; bilder/"; int offset = 0; // wenn mehrere Monitore angeschlossen ScreenRegion region; region; Mouse mouse; mouse; Keyboard keyboard; keyboard; Anmerkung: Sikuli-IDE hat ein Positionierungsproblem, wenn mehr als ein Monitor angeschlossen ist und der erste Monitor nicht der Hauptbildschirm ist Software-Qualität Stephan Kleuker 290 Sikuli-Nutzung (4/10): mehrere Monitore public GUIBedienung() GUIBedienung() { // nur bei mehreren Monitoren GraphicsEnvironment ge = GraphicsEnvironment .getLocalGraphicsEnvironment(); getLocalGraphicsEnvironment(); GraphicsDevice[] GraphicsDevice[] gs = ge.getScreenDevices(); ge.getScreenDevices(); GraphicsConfiguration[] GraphicsConfiguration[] gc = gs[0]. gs[0].getConfigurations [0].getConfigurations(); getConfigurations(); System.out.println( System.out.println(gc[0]. gc[0].getBounds [0].getBounds(). getBounds().getX ().getX()); getX()); if (gc[0]. gc[0].getBounds [0].getBounds(). getBounds().getX ().getX() getX() > 0) { this.offset = (int (int) int) gc[0]. gc[0].getBounds [0].getBounds(). getBounds().getX ().getX(); getX(); } if (!System.getProperty (!System.getProperty("os.name"). System.getProperty("os.name").startsWith ("os.name").startsWith("Windows")) startsWith("Windows")) { System.err.println("Sorry, System.err.println("Sorry, nur fuer Windows umgesetzt"); } this.mouse = new DesktopMouse(); DesktopMouse(); this.keyboard = new DesktopKeyboard(); DesktopKeyboard(); } Software-Qualität Stephan Kleuker 291 Sikuli-Nutzung (5/10): Starten (individuell) // hier liegt zu testendes Programm als JarJar-Datei vor, die // aufgerufen wird (bestriebssystemabhängig (bestriebssystemabhängig ). // auch direkter Aufruf des Konstruktors ggfls. möglich public void initialisieren() { String[] cmd = new String[3]; cmd[0] cmd[0] = "cmd.exe"; cmd[1] cmd[1] = "/C"; cmd[2] cmd[2] = "java "java -jar tarifrechner.jar"; try { Runtime.getRuntime(). Runtime.getRuntime().exec ().exec( exec(cmd); cmd); Thread.sleep(1000 Thread.sleep(1000); (1000); // evtl. nicht notwendig } catch (Exception (Exception e) { e.printStackTrace(); e.printStackTrace(); throw new IllegalArgumentException(" IllegalArgumentException("Start ("Start gescheitert"); gescheitert"); } region = new DesktopScreenRegion( DesktopScreenRegion(-offset, 0, 300, 275); } Software-Qualität Stephan Kleuker 292 Sikuli-Nutzung (6/10): Hilfsmethoden private ScreenRegion klickpunkt(ScreenRegion klickpunkt(ScreenRegion r, int x, int y){ Rectangle rec = r.getBounds(); r.getBounds(); rec.x += offset + x; rec.y += y; rec.height = 0; rec.width = 0; r.setBounds( r.setBounds(rec); rec); return r; } private ScreenRegion findeBild(String findeBild(String name) name) { return findeBild( findeBild(name, name, 0.9); } private ScreenRegion findeBild(String findeBild(String name, double score) { File file = new File(this.verzeichnis File(this.verzeichnis + name); name); Target target = new ImageTarget( ImageTarget(file); file); target.setMinScore(score); target.setMinScore(score); return region.find( region.find(target); target); Stephan Kleuker 293 } Software-Qualität Sikuli-Nutzung (7/10): GUI-Elemente ansteuern public void beenden() { ScreenRegion r = findeBild("Beenden.png"); findeBild("Beenden.png"); r = klickpunkt(r, 241, 8); this.mouse.click( this.mouse.click(r.getCenter()); r.getCenter()); } public void zonenEingeben(String zonenEingeben(String eingabe) eingabe) { ScreenRegion r = findeBild("Zonen.png"); findeBild("Zonen.png"); r = klickpunkt(r, 82, 12); this.mouse.click( this.mouse.click(r.getCenter()); r.getCenter()); this.keyboard.type( this.keyboard.type(eingabe); eingabe); } public void alterUeber64Waehlen() { ScreenRegion r = findeBild("AltersBox.png"); findeBild("AltersBox.png"); r = klickpunkt(r, 157, 12); this.mouse.click( this.mouse.click(r.getCenter()); r.getCenter()); r = findeBild("AlterAendern.png"); findeBild("AlterAendern.png"); r = klickpunkt(r, 117, 53); this.mouse.click( this.mouse.click(r.getCenter()); r.getCenter()); } Software-Qualität Stephan Kleuker 294 Sikuli-Nutzung (8/10): Ergebnis finden public String berechnenAusfuehren() berechnenAusfuehren() { ScreenRegion r = findeBild("Berechnen.png"); findeBild("Berechnen.png"); r = klickpunkt(r, 63, 16); mouse.click( mouse.click(r.getCenter()); r.getCenter()); r = findeBild("30Cent.png", findeBild("30Cent.png", 0.887); if (r != null) return "30 Cent"; Cent"; r = findeBild("130Cent.png", findeBild("130Cent.png", 0.82); if (r != null) return "130 Cent"; Cent"; r = findeBild("220Cent.png", findeBild("220Cent.png", 0.86); if (r != null) return "220 Cent"; Cent"; r = findeBild("175Cent.png", findeBild("175Cent.png", 0.81); if (r != null) return "175 Cent"; Cent"; throw new IllegalArgumentException( IllegalArgumentException( "Unerwartetes Ergebnis"); } Software-Qualität Stephan Kleuker 295 Sikuli-Nutzung (9/10): Tests (1/2) wiederverwandt // in GuiTarifrechnerTest private GUIBedienung gui; gui; // in setUp() setUp() intialisieren @Test public void test1() { gui.zonenEingeben("1"); gui.zonenEingeben("1"); gui.alterUnter14Waehlen(); gui.teuerUhrWaehlen(); gui.teuerUhrWaehlen(); gui.zumBetriebKlicken(); gui.zumBetriebKlicken(); Assert.assertTrue( Assert.assertTrue(gui.berechnenAusfuehren() gui.berechnenAusfuehren() .equals("30 equals("30 Cent")); } @Test public void test2() { gui.zonenEingeben("2"); gui.zonenEingeben("2"); gui.alter1464Waehlen(); gui.billigUhrWaehlen(); gui.billigUhrWaehlen(); Assert.assertTrue( Assert.assertTrue(gui.berechnenAusfuehren() gui.berechnenAusfuehren() .equals("130 equals("130 Cent")); } Software-Qualität Stephan Kleuker 296 Sikuli-Nutzung (10/10): Tests (2/2) wiederverwandt @Test public void test3() { gui.zonenEingeben("1"); gui.zonenEingeben("1"); gui.alter1464Waehlen(); gui.billigUhrWaehlen(); gui.billigUhrWaehlen(); gui.zumBetriebKlicken(); gui.zumBetriebKlicken(); Assert.assertTrue( Assert.assertTrue(gui.berechnenAusfuehren() gui.berechnenAusfuehren() .equals("30 equals("30 Cent")); } @Test public void test4() { gui.zonenEingeben("2"); gui.zonenEingeben("2"); gui.alterUeber64Waehlen(); gui.teuerUhrWaehlen(); gui.teuerUhrWaehlen(); Assert.assertTrue( Assert.assertTrue(gui.berechnenAusfuehren() gui.berechnenAusfuehren() .equals("220 equals("220 Cent")); } Software-Qualität Stephan Kleuker 297 Sikuli-Ausblick • Sikuli-API mit Potenzial, aber wenig Entwickler • Variante Sikuli-Script, Sikuli X (http://www.sikuli.org/) – ähnliches Konzept, aber mit Oberfläche und Skriptsprache, die das Suchen, Analysieren und Nutzen von Bildern vereinfacht – benötigt (e?) Java 6 – Nutzung zusammen mit Eclipse recht aufwändig [Kle] – aktuell (2014) vielversprechende Aktualisierung • typisches Weiterentwicklung: Texterkennung Software-Qualität Stephan Kleuker 298 Sikuli-Skript Software-Qualität Stephan Kleuker 299 Fazit • GUI-Tests sind generell möglich • GUI-Tests erst planen, wenn GUI relativ festgelegt ist • Werkzeuge kritisch evaluieren, ob sie für Projekt bzw. typische Unternehmensaufgaben geeignet sind • freie GUI-Werkzeuge können auf jeden Fall Einstieg in GUITestansätze sein • Evaluation von kommerziellen Werkzeugen in diesem Bereich oft sinnvoll • (wieder) können Kombinationen von Werkzeugen sinnvoll sein • generell wird Teststrategie benötigt, was wann getestet wird (botton up, top down, middle out) Software-Qualität Stephan Kleuker 300