C++-arkivet
|
Tappa kontrollenTestdriven utveckling (eng: test driven development) och agila utvecklingsmetoder (eng: agile) förutsätter bra enhetstesning (eng: unit testing). Enhetstestning handlar om att skriva kod som testar kod. Man skriver sina tester för att snabbt få reda på om ens programkod fungerar efter en ändring. Enhetstestning är det billigaste sättet att få bra kvalitet på din applikation. Dels för att du kan undvika den kombinatorik man får om man testar hela applikationen samtidigt, men även för att du då hittar buggar så tidigt som möjligt efter att koden skrevs.
För att maximera fördelarna med enhetstestning måste dina tester vara lätta att köra, de måste köra snabbt och alltid ge samma resultat. Det är tyvärr inte alltid så lätt att åstadkomma... Vad händer t.ex. om din kod använder klasser ur ett bibliotek för nätverkskommunikation (NetworkComm)? Kommunikationslogiken (CommLogic) i din applikation har en funktion sendRequest som använder NetworkComm för att skapa en ändpunkt, sätta upp en förbindelse och skicka meddelandet (se Figur 1).
Du vill skriva tester för kommunikationslogiken i CommLogic. Då måste någon ge dig svar på de förfrågningar du skickar över nätverket. Du måste alltså starta ett externt program för att få svar. Frågan är om det går snabbt nog och alltid ger samma svar (tänk tusentals testfall)? Och att starta externa program är krångligt. Det bästa vore om dina tester var helt fristående. Kommunikation med världen utanför ditt program är bara ett exempel på svårigheter vid testning. De flesta problem man stöter på faller inom någon av följande kategorier:
Stöter du på ovanstående problem har du helt enkelt inte full kontroll över alla delar av testfallet. Det blir krångligt att testa. Det måste finnas ett bättre sätt. ArvlösSom vi såg i interfaceskolan del 3: testbarhet så kan man bli av med en problematisk typ (nätverkskommunikationen i exemplet ovan) genom att skapa ett interface. Man låter den problematiska typen ärva från interfacet. Vi testning kan man sen skapa en testklass som ärver från interfacet. Testklassen gör precis det du vill, och du har återfått kontrollen! I Figur 1 visas åter kommunikationslogiken CommLogic som använder nätverkskommunikation genom klassen NetworkComm. Med ett interface bryter man detta direktberoende och kan även skapa en testklass (NetworkCommMock), se Figur 2:
Men, vad händer om du inte kan ärva? Det kan t.ex. vara så att du använder klasser ur Boosts förträffliga asio-bibliotek (asynchronous I/O). Du vill inte gärna modifiera bibliotekskoden. Så vad göra? Typer man saknar kontroll överDet är tyvärr ganska vanligt att man använder kod man inte har kontroll över. Här är några exempel på orsaker till att man inte kan använda en interfaceklass på det sätt vi pratade om ovan:
Stöter vi på någon av de ovanstående exemplen så har vi alltså ett problem vi inte kan lösa. Så vad göra? OmdirigeradDavid Wheeler (1927-2004) var den datalog som myntade uttrycket "Any problem in computer science can be solved with another layer of indirection" (på svengelska ungefär: en extra omdirigering). Och ojojoj, om han visste vad han pratade om. Denna observation går att tillämpa på allt, från lägsta nivå (t.ex. pekare) till mest abstrakta nivå (såsom design patterns). Ett bra exempel på extra omdirigering såg vi i Figur 2. Istället för att CommLogic använder NetworkComm direkt så pratar CommLogic via interfacet till NetworkComm. Jämför detta med vårt dilemma: vi får problem med testning för att vi använder en problematisk typ (t.ex. en biblioteksklass). Vi behöver bryta beroendet till den problematiska typen. Vi lägger till ett interface! Men vänta! Har vi inte redan diskuterat om hur tråkigt och jobbigt det var att vi inte kunde ärva ett interface. Nyckelordet här är "ärva". Våra problematiska datatyper ska inte ärva vårt nya interface. HjälpÅter till exemplet ovan. Låt säga att klassen NetworkComm är del av ett bibliotek. Vi har alltså ingen kontroll över koden och kan inte ärva som i Figur 2. Vi inför istället en hjälptyp NetworkCommAccess (se Figur 3). Behöver vi något från NetworkComm så ber vi accessobjektet om detta. NetworkCommAccess skickar sedan förfrågan vidare till NetworkComm.
Så vad har vi åstadkommit? Nästan ingenting! CommLogic beror fortfarande på NetworkComm, fast nu via NetworkCommAccess. Och vår testning har inte blivit något lättare alls. Men vi har kommit en bit på väg. Nu har vi nämligen en klass som vi har kontroll över! Inget gott som inte har något ont med sigDavid Wheelers citat löd egentligen "Any problem in computer science can be solved with another layer of indirection. But that usually will create another problem." Och det är detta som är kruxet. Allt som är bra har alltid någonting dåligt med sig. (Jag måste bara få citera en helgalen lunchkonversation angående bra och dåligt: "Men lax, det är ju både gott och nyttigt, så vad skulle vara dåligt med lax?" -"Men varför äter du inte lax hela dagarna istället för att jobba? Jo, för att det är dyrt och bara är nyttigt i rimliga mängder!" Slutsats: om det fanns något som bara var bra så skulle man göra det och inget annat! :) I vårt fall är nackdelen ökad komplexitet. Som vi sa förut, för att få bra testbarhet måste vi införa ett interface (se Figur 4):
Vårt nya interface NetworkCommAccessInterface har funktioner för att lätt komma åt nätverket (setupAndSend). Vi har även lagt till en testklass NetworkCommAccessMock. Denna klass kommer att avlyssna alla anrop som CommLogic gör till NetworkCommAccessInterface. På så sätt kan den kontrollera att kommunikationslogiken fungerar som den ska. CommLogic behöver få tillgång till rätt klass att prata med: antingen produktionsklass eller testklass. Interfaceskolan del 3: testbarhet beskrev en teknik som heter beroendeinjektion (dependency injection): Vi låter CommLogics konstruktor ta en referens till en NetworkCommAccessInterface. Produktionskoden skickar en NetworkCommAccess till konstruktorn. Testkoden skickar en NetworkCommAccessMock till konstruktorn. Fördelar och FacadeLägg märke till att det bara är CommLogic och NetworkComm som är nödvändiga för vår produktionskod! Det verkar inte bättre än att vi lagt till tre klasser bara för att få testbarhet. Det är dock inte riktigt sant. Vår kod har samtidigt blivit bättre.
För det andra så har NetworkCommAccess minskat kopplingen (eng: coupling, alltså hur mycket den ena beror på den andres implementation) mellan våra klasser CommLogic och NetworkComm. Men vadå?, säger du. Man har ju bara flyttat anropen så att de går via hjälpklassen NetworkCommAccess. Jovisst, men skillnaden är att vi har infört en abstraktion i NetworkCommAccess. Vi använder inte längre createEndpoint, setupConnection och send. Istället använder vi en funktion setupAndSend som jobbar på en högre nivå. Vill vi ändra hur vi skickar nätverksmeddelanden är det bara att ändra i setupAndSend. CommLogic påverkas inte. Nyckeln till låg koppling är alltså ett stabilt gränssnitt mellan klasserna. Samma idé ligger bakom designmönstret Facade (eng: the Facade design pattern, från boken Gamma et. al. "Design patterns - Elements of reusable object-oriented software"). Facade tar ett komplext delsystem (t.ex. nätverkskommunikation) och döljer det bakom ett enkelt gränssnitt. Gränssnittet mellan subsystemen blir mer stabilt och kopplingen därmed lägre. Nackdelen med Facade är att man döljer en del funktionalitet som då inte går att komma åt. SammanfattningDet vi diskuterat är ett mönster som du kan använda då du har en datatyp User som du vill testa men som beror på en datatyp Problematic som gör testning svårt.
Du skapar alltså en ny hjälpklass (Access) och låter den ärva från ett enkelt interface AccessInterface. User pratar nu bara med interfacet. Access tar förfrågan och skickar den vidare till Problematic.
User kan nu testas genom att skicka in AccessMock som även den ärver från AccessInteface. Relaterade artiklar:
|