2012-03-18

TDD utflykt i OpenLaszlo land, del 1

Egentligen tycker jag inte om Flash. Det beror kanske mest på alla blinkande och irriterande reklam som finns på en del webbsajter. De får min högst ålderdomliga laptop till att sega ihop. Dessutom upptäcks det titt som tätt nya säkerhetshål i Flash.

Jag fick en idé till en mystery som jag inte riktigt visste hur jag skulle genomföra. Den kräver ett visst mått av interaktivitet. Mysteryt går ut på att man ska, givet ett antal mynt i ett visst mönster, flytta ett av mynten för att bilda ett nytt mönster. Som illustration i det här inlägget ligger mynten placerade enligt bilden nedan. Det gäller att flytta ett av mynten och bilda en kvadrat.

Kanske skulle det vara möjligt att använda Flash? Jag surfade runt lite för att ta reda på hur man skapar Flash applikationer. Adobe har verktyg för detta. Men efter att ha tittat på deras program och fått skrämselhicka av priserna insåg jag att det var uteslutet. Det billigaste jag kunde hitta var Adobe Flash Builder 4.5 (standard edition) för 2175 kr, vilket är ungefär 2000 kr mer än jag är villig att betala. Jag har inga planer på att börja en karriär som Flash-programmerare. Jag vill bara göra en enkel applikation för ett mystery.

Efter ytterligare lite letande på nätet hittade jag ett gratis alternativ i OpenLaszlo, vilket såg lovande ut.




Det började inte så bra. Jag laddade ner OpenLaszlo och installerade utan problem. Men när jag försökte starta OpenLaszlo servern hände ingenting. Efter lite felsökande kom jag fram till att det var JDK som spökade. Jag har haft det en gång i tiden men avinstallerat det. Dock fanns det en miljövariabel kvar som lurade OpenLaszlo installationen att tro att JDK fanns på maskinen. Efter att ha installerat JDK så fungerade det fint.

Till min stora glädje såg jag att det även fanns ett xUnit-ramverk till OpenLaszlo.

När man närmar sig ett helt okänt område kan det vara svårt att veta var man ska börja. Men TDD gör det enkelt. Börja med ett fallerat test. Följande kod hittade jag på nätet. Den är nästan precis vad jag är ute efter.

<canvas debug="true"> <debug y="150"/> <include href="lzunit"/> <TestSuite> <TestCase> <method name="test"> </method> <method name="addTests"> this.addTest("test"); </method> </TestCase> </TestSuite> </canvas>

Lite kort om koden ovanför. Det yttersta elementet (canvas) utgör rotelementet för en OpenLaszlo applikation och definierar det utrymme (eller duk om man så vill) som övriga visuella komponenter finns på. Attributet debug betyder att applikationen ska köras i debug-läge. Nästa element (debug) talar om var det extra debug-fönstret ska visas. Själva xUnit-ramverket importeras genom att inkludera lzunit.

Därefter följer testfallen, eller snarare testfallet, som utgörs av metoder i ett TestCase-element. Vidare måste testfallen (metoderna) explicit läggas till. Detta görs genom att ange dem i en särskild metod addTests. TestCase-element omsluts av ett TestSuite-element. Det hela sparas som firsttest.lzx under OpenLaszlo servern. Vid en körning fås följande resultat.

Grått är varken grönt eller rött.

<method name="test"> assertTrue(false); </method>

För att få rött krävs att testet fallera. En hårdkodad assert tar hand om det.

Som synes är informationen om vilket testfall som fallerat ganska så sparsmakad. Det går att ange namn för både TestSuite och TestCase (/TestSuite respektive /anonymous ovan) via antingen name-attributet eller genom att härleda klasser från TestSuite och TestCase. Det sista (/test) är metodens namn. Det verkar som att alla metoder som utgör testfall måste inledas med test. Lite onödigt kan man tycka med tanke på att de ändå måste läggas till explicit i addTests.

Givetvis blir det grönt om man ändrar till assertTrue(true)... Nu när enhetstestramverket är på plats är det bara till att börja TDD:a.




Den lilla design jag har gjort i huvudet går ut på att det finns en klass för att hantera mynt. Ett mynt har en startposition samt en eventuell slutposition. Om användaren tar rätt mynt och lägger det på rätt slutposition ska ett meddelande visas som talar om att det är rätt. Annars visas ett meddelande som säger att det är fel.

Eftersom OpenLazslo är helt nytt för mig tänker jag börja med ett väldigt enkelt test. Testfallen för klassen som hanterar mynt läggs i en egen klass döpt till CoinTests. Testfallen körs genom att inkludera dem i huvudfilen (UnitTests.lzx):

<canvas debug="true"> <debug y="150"/> <include href="lzunit"/> <include href="CoinTests.lzx" /> <TestSuite> <CoinTests /> </TestSuite> </canvas>

Första testfallet blir som följer. Normalt finns det sällan anledning att testa så triviala saker som att skriva och läsa attribut. Men som sagt, det här är helt nytt för mig, så det känns bra att börja med något som borde fungera utan problem.

<library> <class name="CoinTests" extends="TestCase"> <method name="testInitialPosition"> <Coin name="coin" x="10" y="20" /> assertEquals("10", coin.x); assertEquals("20", coin.y); </method> <method name="addTests"> this.addTest("testInitialPosition"); </method> </class> </library>

Självklart så går det inte att kompilera koden. Det finns ingen klass Coin än. Men det är inte där skon klämmer.

  CoinTests.lzx:3:44: The class tag cannot have child tags in this context

Det betyder att man inte kan instansiera objekt inne i metoder. En helt klart intressant begränsning. Det finns kanske en förklaring och motivering till detta i dokumentationen. Men den har jag knappt titta i den, så jag vet inte.

<library> <class name="CoinTests" extends="TestCase"> <Coin name="coin" x="10" y="20" /> <method name="testInitialPosition"> assertEquals("10", coin.x); assertEquals("20", coin.y); </method> <method name="addTests"> this.addTest("testInitialPosition"); </method> </class> </library>

Lösningen blir att flytta upp instansieringen en nivå, dvs coin kommer bli en medlem i testfallet. Då gick det bättre.

  CoinTests.lzx:2:48: The tag 'Coin' cannot be used as a child of class CoinTests.lzx:3:32: Unknown tag <Coin>

En minimal Coin klass, samt inkludera den i testfallet.

<library> <class name="Coin" /> </library>

Nu gick det ännu bättre. Till och med för bra. Testfallet gick igenom trots att x och y inte är deklarerade. Tydligen finns det implicita attribut, därför fungerade det direkt. För säkerhets skulle provade jag att ändra x till 11 och då fallerade testfallet.




Ett par reflektioner så här långt:

  • Vid testdriven utveckling är det smidigt om man kan skapa nya objekt vid behov, vanligtvis i varje enskilt testfall. Annars måste man hela tiden se till att objekten har samma väldefinierade tillstånd när respektive test körs. Klassen TestCase har förvisso både en setUp- och en tearDown-metod som kan användas för detta. Men det finns alltid en risk för att något går snett när gamla objektinstanser återanvänds.
  • Eftersom man måste explicit ange den metod som utgör testet är det viktigt att varje nytt testfall fallerar. Om inte kan man tro att koden är korrekt, när det egentligen är så att man inte kör det nya testfallet.
  • Det borde vara tillräckligt med TestCase som den minsta enheten. Det går bra att lägga ett antal TestCase i en TestSuite. Det går också bra att lägga ett antal TestSuite efter varandra.

Givet dessa synpunkter funderar jag på att skapa en ny klass härled från TestCase vars syfte ska vara att fungera som ett enkelt testfall. Den skulle i så fall ta hand om anropet till addTests. Det enda man skulle behöva göra är att implementera en test-metod. Men jag låter det stanna vid en tanke än så länge. Det känns lite märkligt att det första man gör är att utöka det befintliga ramverket för enhetstest.




Dags för nästa test. När man har flyttat ett mynt ska resultatet av flytten visas. Det verkar enklast om det flyttade myntet talar om för förälder-objektet vilket resultat det är som ska visas. Okänd mark innebär små steg. I vårt fall är TestCase förälder-objektet. En korrekt flytt signaleras genom att en variabel sätts till sann.

<class name="CoinTests" extends="TestCase"> //... <method name="testMoveToCorrect"> assertTrue(this.correctlyMoved); </method>

Testfallet fungerar inte, av uppenbara skäl.

  ERROR: reference to undefined property 'correctlyMoved'

Genom att lägga till ett attribute så får vi ett fallerat test istället.

<class name="CoinTests" extends="TestCase"> <attribute name="correctlyMoved" value="false" /> //...

Det finns implicita händelser till objekt, till exempel när användaren släpper upp musknappen. Till händelserna kan man knyta skript, antingen direkt eller indirekt via en metod. I C# vet jag hur man gör för att avfyra händelser i testsammanhang. Det kanske går att göra något sådant i OpenLaszlo också, men istället för att börja gräva ner mig i en massa tekniska detaljer väljer jag den enkla vägen. Musknappen simuleras genom att anropa den metod (checkMove) som ska knytas till händelsen. Det är inte optimalt, det finns en risk att man glömmer att göra kopplingen. Men det känns tillräckligt bra för stunden.

<class name="CoinTests" extends="TestCase"> <attribute name="correctlyMoved" value="false" /> <method name="doCorrectMove"> this.correctlyMoved = true; </method> <method name="testMoveToCorrect"> coin.correctX = 10; coin.correctY = 20; coin.checkMove(); assertTrue(this.correctlyMoved); </method>

Jag misstänker att x och y avspeglar ett objekts aktuella position. För att göra det enkelt för mig antar jag att myntet har blivit flyttat till den nuvarande position (10, 20) och att den är den korrekta. För att få testfallet att passera behövs lite kod i Coin-klassen.

<class name="Coin"> <attribute name="correctX" /> <attribute name="correctY" /> <method name="checkMove"> this.parent.doCorrectMove(); </method> </class>

Det fina med TDD är att alla medel, oavsett hur fula de är, är tillåtna för att få ett test att passera. I det här fallet bemödar vi inte oss med att kontrollera om den positionen är den rätta, vi bara antar att den är rätt.

Nästa enklaste tänkbara test som jag kan komma på är att kontrollera att flytt till fel position inte tas som en korrekt flytt.

<method name="testMoveToIncorrect"> coin.correctX = 20; coin.correctY = 30; coin.checkMove(); assertFalse(this.correctlyMoved); </method>

Istället för att flytta på myntet har jag valt att flytta den korrekta positionen. Det känns enklast för tillfället. Uppenbart är att det måste till lite logik i Coin-klassen.

<method name="checkMove"> if (this.x == this.correctX &amp;&amp; this.y == this.correctY) this.parent.doCorrectMove(); </method>

Lägg märke till den logiska OCH-operatorn &amp;&amp;. Antingen skriver man så eller sätter koden mellan <![CDATA[ och ]]>. Det känns lite omständigt och kommer säkert vara en källa till fel. Logiken i sig är väldigt enkel och knappt lönt att testa. Men likväl blev jag överraskad när testet inte passerade.

Problemet är att correctlyMoved har blivit satt i föregående test. Det finns setUp och tearDown i TestCase. Följande gör att testet passerar som tänkt.

<method name="setUp"> this.correctlyMoved = false; </method>

Självklart ska något hända om man flyttar myntet till fel position. På motsvarande sätt ska det finnas en doIncorrectMove i förälder-objektet. Jag skapar inte ett nytt testfall för det utan utökar det befintliga (se nedan). Då passar jag även på att lägga till en verifiering i det andra testet, lika bra att kontrollera att incorrectlyMoved inte blir satt vid korrekt flytt. Implementationen är helt symmetrisk och inget jag visar här.

<method name="testMoveToIncorrect"> coin.correctX = 20; coin.correctY = 30; coin.checkMove(); assertTrue(this.incorrectlyMoved); assertFalse(this.correctlyMoved); </method>

Tillägget i checkMove är enkelt.

<method name="checkMove"> if (this.x == this.correctX &amp;&amp; this.y == this.correctY) this.parent.doCorrectMove(); else this.parent.doIncorrectMove(); </method>

Hittills har vi inte åstadkommit så mycket, vilket är en naturlig följd när man utforskar nya marker. Nu är grunden på plats och det är bara att bygga vidare på det. Härmed är dagens utflykt slut, men en fortsättning kommer i ett senare inlägg.

Inga kommentarer: