Grafische User Interface testen – met Java en Sikuli

In ons dagelijkse werk wordt er veelvuldig gebruik gemaakt van automatische testen met als doel sneller opleveren met een vastgestelde kwaliteit. Grafische User Interface (GUI) testen worden om verschillende redenen minder vaak ingezet. Dit artikel behandelt de positie van GUI testen in het landschap van mogelijke automatische testvormen. Concreet worden ervaringen en best practices gedeeld over “Sikuli”, een populaire en flexibele open source GUI testtool.

Sikuli is een open source grafische scherm analyse tool en is ontwikkeld binnen het Massachusetts Institute of Technology (MIT). Sikuli werkt op basis van images en pixel vergelijking. Hierdoor is Sikuli implementatie onafhankelijk en kunnen er zowel web- als native clients mee getest worden. De Java API van Sikuli is gebruiksvriendelijk, doordat het een klein aantal simpele en intuïtieve methodes bevat. Door deze Java code te combineren met het page object pattern, kunnen er onderhoudbare en overzichtelijke GUI-testen worden gemaakt. Daarnaast maakt de integratie met JUnit het mogelijk om deze testen toe te voegen aan een (bestaande) automatische build. Dit artikel geeft inzicht in het gebruik van Sikuli en de Java API en biedt handvatten om de testen onderhoudbaar op te zetten.

Positionering GUI testen
Om GUI-testen beter te kunnen positioneren wordt er gekeken naar de test automation pyramidDe test automation pyramid (Figuur 1) geeft een indicatie van de verhouding testen per testsoort, dat wil zeggen unit-, integratie of systeem en acceptatie testen. De pyramid geeft geen indicatie wat er getest wordt. Unittesten kunnen bijvoorbeeld ook worden gebruikt om onderdelen van de UI te testen.


Figuur 1: De Test Automation Pyramid

In de pyramid staat de unit test laag voor een solide basis. Wellicht door externe wijzigingen valt er een test om, maar daarmee nog niet de hele testset. Unittesten zijn fijnmazig en testen slechts kleine delen van de applicatie onafhankelijk van elkaar. Wijzigingen aan het systeem raken slechts een klein deel van de unittesten die eventueel onderhoud behoeven.

De tweede laag is de service laag, ook wel de integratielaag genoemd. De service of integratie testen in deze laag controleren of de verschillende componenten of services van een applicatie correct met elkaar integreren. De scope van een integratie test is dus aanzienlijk groter, waardoor wijzigingen aan de applicatie ook sneller kunnen leiden tot het plegen van onderhoud aan de testen.

De top bevat de UI testen. Hiervoor geldt het worst case scenario dat aanpassingen aan het systeem ertoe kunnen leiden dat alle testen in deze laag onderhoud behoeven. De UI of ook wel systeem testen in deze laag testen volledige flows, waarbij een enkele test een groot gedeelte van de applicatie code raakt.

Bij het bepalen van het type test dat men schrijft, kan de stelregel gehanteerd worden dat een test op een zo laag mogelijk niveau wordt geplaatst. Logica in een methode dient getest te worden op unit test niveau. Testen op integratie of GUI testniveau raken deze logica uiteraard wel indirect, maar deze typen testen mogen niet als doel het testen van die methode hebben. Lager in de piramide geplaatste testen zijn niet alleen goedkoper om te maken, maar geven ook sneller nuttige feedback. Des te hoger een test in de piramide, des te meer setup hij behoeft en hoe complexer de test vaak is. Daarnaast kunnen hoger geplaatstse testen pas geschreven worden als er grote delen van de applicatie klaar zijn. Een GUI test wordt niet voor niets vaak een systeem test genoemd. Dit geeft aan hoeveel functionaliteit een applicatie al moet bezitten voor de GUI test gemaakt kan worden. Voor het maken van deze GUI-testen wordt er gebruik gemaakt van Sikuli.

Sikuli
Sikuli werkt op basis van pixel vergelijking. Het zoekt naar images op het scherm en vergelijkt deze met opgeslagen images. Het vergelijken van de images gaat op basis van een threshold, welke default op 80% is ingesteld. Dit betekent dat een image, op pixel niveau, voor 80% overeen moet komen. Naast de threshold kunnen er verschillende instellingen gedaan worden zoals zoeken op geschaalde of gedraaide images. Daarnaast kan Sikuli ook tekst op het scherm herkennen. Door puur grafisch te werken op basis van pixel vergelijking, is Sikuli totaal onafhankelijk van een framework of implementatie. Andere GUI testtools, zoals Rational Functional Tester, SWTBot of Selenium, haken direct in op specifieke GUI frameworks, en zijn daardoor complexer en sterker gekoppeld aan de gebruikte technologie.

De functionaliteit van Sikuli wordt ontsloten door een Java API. De twee belangrijkste klassen in deze API zijn DesktopScreenRegion, DesktopMouse en ImageTarget. De DesktopScreenRegion wordt gebruikt om het scherm te benaderen, en biedt methodes om images op het scherm te vinden en om delen van het scherm te benaderen. De DesktopMouse klasse wordt gebruikt om muisklikken te kunnen simuleren op het scherm. Sikuli werkt op basis van images, de ImageTarget klasse is de Sikuli implementatie van een image.

De basis van Sikuli werkt erg simpel, zie het code voorbeeld in Figuur 3. Maak een instantie van DesktopScreenRegion en een ImageTarget aan. Door de find methode wordt het image op het scherm gevonden. De methode retourneert een ScreenRegion waar het gevonden image zich bevindt, en null als het niet gevonden wordt. Via de DekstopMouse klasse kan er geklikt worden op coördinaten in het scherm. Deze krijgt als waarde mee het centrum van de gevonden ScreenRegion.


ScreenRegion screen = newDesktop ScreenRegion();
ImageTarget button = new ImageTarget(new File(“c:\images\buttonImage.png”));
ScreenRegion buttonRegion = screen.find(target);
DesktopMouse mouse = new DesktopMouse();
mouse.click(buttonRegion.getCenter());

Figuur 3: Sikuli Code voorbeeld

De ImageTarget klasse kan aangemaakt worden door middel van een File maar ook door een URL. Verder bied de klasse de mogelijkheid om de evaluatie criteria in te stellen. Denk daarbij aan criteria als het percentage wat overeen moet komen (threshold). Het code voorbeeld in Figuur 4 toont het gebruik van ImageTarget waarbij de setMinScore methode het threshold instelt op 95%.


ImageTarget target = new ImageTarget(new File(“c:\images\buttonImage.png”));
target.setMinScore(0.95d);

Figuur 4 Pattern Code Voorbeeld

Het Page Object Pattern
GUI-testen vergen vaak veel setup, dit maakt de setup code lang, onleesbaar en niet onderhoudbaar. Het pageobject pattern[5] biedt een oplossing voor deze problematiek. De pageobjecten modeleren een applicatie aan de hand van zijn schermen en dialogen. Pageobjecten zijn in code geschreven representaties van grafische schermen of delen van deze schermen. Een grafisch scherm bevat een aantal knoppen, of andere interactie punten of toont een presentatie van data.

Het pageobject modelleert elk scherm aan de hand van zijn interacties. Elke interactie van een scherm wordt een methode van het pageobject. Als een interactie op een scherm resulteert in het openen van een scherm of dialoog, zal pageobject dit ook implementeren. De methode van het pageobject zal een pageobject van het nieuwe scherm terug geven. De unittesten benaderen de applicatie door middel van zijn pageobjecten.

Het onderstaande sequence diagram (Figuur 5) toont hoe vanuit een unittest de applicatie gestart en ingesteld kan worden. De starter klasse zorgt voor het opstarten van de applicatie. Na het starten van de applicatie geeft de klasse een pageobject terug van het login dialoog. Het pageobject geeft toegang tot het logindialoog. Het dialoog pageobject biedt de mogelijkheid om in te loggen. Deze geeft dan een pageobject van het applicatie hoofdscherm terug. De unittest opent vervolgens een dialoog om instellingen aan de applicatie te doen.


Figuur 5: Sequence Diagram

De implementatie van een pageobject wordt toegelicht aan de hand van een voorbeeld. Er wordt een pageobject gecreëerd voor een opslaan dialoog (Figuur 6). Het dialoog kent een viertal acties:

  • Opslaan
  • Annuleren
  • Browse
  • Bestandsnaam opgeven


Figuur 6 Opslaan dialoog

Het pageobject voor dit dialoog zal deze acties als methodes implementeren. De code voor het basis pageobject ziet er uit als het codevoorbeeld in Figuur 7. De constructor zoekt het dialoog op basis van een image. Het gevonden image wordt opgeslagen als de Region waarbinnen de verdere zoekopdrachten gedaan worden. Dit versnelt het zoekproces aanzienlijk omdat je nu alleen binnen een bepaalde region verder hoeft te zoeken en niet het hele scherm.


 public class SaveDialoog{
  private ScreenRegion dialoog;
 
  public SaveDialoog(){
                ScreenRegion screen = new DesktopScreenRegion();
                                  ImageTarget dialoogImage  = new ImageTarget ("C:\images\VolledigeDialoog.png");
                                  dialoog = screen.find(dialoogImage);
  }
}

Figuur 7 Code Voorbeeld page object

De klasse wordt daarna uitgebreid met de acties van het dialoog. Opslaan en annuleren zijn eenvoudig te implementeren, door een clickmethode aan te roepen via een image region van de button. Het gebruik van de region helpt hierbij, hierdoor is er de zekerheid dat er nergens anders op het scherm een zelfde button staat. De browse button opent een nieuw dialoog. Het aanroepen van deze methode returned dan ook een pageobject van dit nieuwe dialoog.

Naast het opslaan en annuleren kan er een browse dialoog geopend worden. De browse methode geeft dan een pageobject terug voor het browse dialoog dat geopend zal worden op het scherm. De laatste actie is het opgeven van een bestandsnaam. Voer een klik uit in het centrum van het image uit Figuur 9. Hierdoor zal de cursor in het bestandsnaam veld komen te staan. Daarna kan er via de DesktopKeyboard klasse tekst in geplaatst worden.


Figuur 9: Sikuli zoek image

JUnit
Door de pageobjecten te gebruiken wordt een setup van een unittest overzichtelijk en leesbaar. De setup vergt vaak niet meer dan een aantal regels code. Het volgende code voorbeeld (Figuur 9) laat zien hoe de setup van een unittest in de applicatie er uit ziet. Dit is de implementatie van het sequence diagram uit Figuur 5.

Het opstarten van de client en het inloggen is iets wat bij voorkeur gedaan wordt in de setup van de klasse en niet voor elke unittest opnieuw. Dit kan door een setup methode met de @BeforeClass tag op te nemen. Het afsluiten gebeurt dan in een teardown methode met een @AfterClass tag.

De client moet voor elke unittest terug gebracht worden naar een bepaalde uitgangspositie. Een falende unittest heeft hierdoor geen invloed op de opvolgende testen. Dit kan zelfs betekenen dat het verstandig is om de gehele client af te sluiten en opnieuw te starten.

Na de setup van de test moeten de controles uitgevoerd worden. Sikuli assert de client aan de hand van images. Deze images representeren de client met de juiste schermen en met de juiste gegevens op het scherm. Hiervoor wordt gebruik gemaakt van de find methode in Sikuli in combinatie met de default threshold van 80% of door deze in te stellen (Figuur 10).


LoginDialoog loginDialoog = new MidClientOpstarter().startMidClient();
Client client = loginDialoog.login("gb", "gb");
InstellenInhoudDialoog dialoog = client.openInstellenInhoudDialoog();
dialoog.setTijd(Ma, 12:00, Di, 4:00);
dialoog. …
dialoog.ok();

Figuur 10: Setup Code van een Unittest

Uitdagingen
Bij het maken van de testen zijn er de volgende uitdagingen:

  • GUI-testen zijn breekbaar en vergen in vergelijking tot integratie of unit testen veel onderhoud.
  • Het zoeken van images op het scherm is langzaam;
  • GUI-testen moeten een zinnige foutmelding geven;
  • Instellen van de threshold voor een image;

Onderhoudbaarheid GUI-testen
Het pageobject pattern scheidt de setup test code van de daadwerkelijke testen, wat hergebruik over verschillende testen bevordert. Door gebruik te maken van het pattern worden de testen leesbaarder en beter onderhoudbaar. Door de applicatie op te delen in schermen hoeft men alleen het pageobject van het gewijzigde scherm aan te passen. Dit zal op de GUI testen zelf weinig tot geen effect hebben.

Performance bij zoeken
Pageobjecten maken gebruik van de Screen klasse om toegang te krijgen tot het scherm. Om de zoekopdrachten te versnellen wordt het scherm opgedeeld in regions. Dit opdelen kan door het opgeven van coördinaten, maar ook op basis van de zoek resultaten (Figuur 7). Niet alleen versnelt het opdelen de testen maar voorkomt het ook de kans op fouten. Vooral bij het zoeken naar images die vaker kunnen voorkomen op het scherm, denk hierbij aan een ‘Ok’ knop.

Foutmeldingen
Zoals altijd met het schrijven van code is het ook voor GUI testen erg belangrijk zinnige foutmeldingen te genereren. Sikuli geeft null terug als een image niet gevonden wordt op het scherm. Er moet een keuze gemaakt worden om dit af te handelen in de unittest d.m.v. een exception of in het pageobject. De keuze valt op het direct afhandelen in het pageobject (Figuur 11). Hier kan namelijk een duidelijke foutmelding gegenereerd worden op basis van de bekende gegevens.


DesktopScreenRegion screen = new DesktopScreenRegion();
ImageTarget imageTarget = new ImageTarget(new File("c:\images\image.png"));
imageTarget.setMinScore(0.99d);
ScreenRegion exists = screen.find(imagePattern);
if (exists == null) {
     fail("Het image wordt niet of niet juist weer gegeven.");
}

Figuur 11: Voorbeeld “assertion” using Sikuli


 
public void cancel(){
          ImageTarget button = new ImageTarget(new File(“c:\images\cancelButton.png”));
          ScreenRegion buttonRegion = dialoog.find(button);
        
         if(buttonRegion != null){
               mouse.click(buttonRegion.getCenter());
         } else {
                   fail(“De cancel button kan niet gevonden worden.” +                             SikuliTool.saveScherm(dialoog, “C:\images\cancelButton.png”));
        }
}

Figuur 12 Fail in pageobjecten

Het pageobject handelt een niet gevonden image af door een fail aan te roepen. De fail methode faalt de unittest die gebruik maakt van het pageobject. Best practice is het maken van een screenshot om de foutmelding te ondersteunen. Er is een eigen SikuliTool klasse gemaakt welke op basis van een meegegeven Region een screenshot maakt en saved. De save methode geeft een melding terug met daarin de locatie van het screenshot en een verwijzing naar het gezochte image op het scherm. Deze visuele feedback op de falende unittesten is zeer waardevol tijdens het debuggen en om te bepalen waarom een nightly build gefaald is.

Instellen threshold
Om testen betrouwbaar te maken is het instellen van een goede threshold belangrijk. Als de threshold te licht ingesteld wordt zal Sikuli te veel herkennen. Met een te zwaar ingestelde threshold kunnen testen falen. Een threshold van 100% is niet aan te raden. Er treden in een client altijd kleine verschillen op door anti aliasing of een schaduw die net iets anders berekend is. Voor native clients kunnen verschillende Windows themes ook problemen geven. Als best practice wordt een threshold van 98% aangeraden als iets volledig goed op het scherm dient te staan. Sikuli herkent de meeste GUI items met de default instellingen.

Conclusie
Ondanks dat de Sikuli tool zich nog in versie 1.0 RC3 bevindt wordt het al twee jaar ingezet bij een klant van ons. Het heeft ons al meerdere malen nuttige feedback gegeven. De falende GUI testen zijn een indicatie van missende unittesten. Deze moeten dan ook uitgebreid worden. De roadmap voor de 1.0 release is sinds november 2012 beschikbaar evenals een standalone Java API.

Het gebruik van het pageobject pattern komt de onderhoudbaarheid van de testen ten goede en wordt als zeer prettig ervaren. Een aanpassing aan een venster in de client betekent niet meer dan een aanpassing aan een pageobject, zonder de unittesten aan te hoeven passen. Mede door een goede foutmelding met het bijgeleverde screenshot waren de falende testen snel te debuggen. Het kwam niet vaak voor dat een test opnieuw gestart moest worden en een ontwikkelaar de acties op het scherm moest volgen om de fout te ontdekken. De screenshots zijn een uitkomst.

Sikuli en pageobject pattern maken het makkelijk om GUI testen te schrijven. Sikuli heeft een korte leercurve. Een ontwikkelteam heeft het binnen een paar dagen goed onder de knie. Hierdoor kan er met weinig tijd en moeite een onderhoudbare en begrijpbare set GUI testen gecreeerd worden. Dat is de grote kracht bij het gebruik van Sikuli met het pageobject pattern.

 

http://blog.mountaingoatsoftware.com/the-forgotten-layer-of-the-test-automation-pyramid

http://www-01.ibm.com/software/awdtools/tester/functional/

http://eclipse.org/swtbot/

http://seleniumhq.org/

http://www.theautomatedtester.co.uk/tutorials/selenium/page-object-pattern.htm