Zoals je wellicht weet, kun je in Visual Studio 2005 Unit Testen. Dat houdt in dat je testcode kunt schrijven die je classes, methods, properties en events kan testen. Erg handig, want als je later ergens in je implementatie iets wijzigt, hoef je alleen maar je unit-tests opnieuw te draaien om er min of meer zeker van te zijn dat je de functionaliteit niet om zeep geholpen heb.
Uiteraard werkt dat alleen maar als je ook echt alle code test. Als in je implementatie code hebt staan die niet door je unit tests getest wordt, kun je er dus nooit zeker van zijn dat je bij het refactoren de boel niet in de war brengt.
Gelukkig biedt Visual Studio 2005 daar een oplossing voor: de code-coverage meting. Dat doet niets meer dan bij het draaien van je unittests kijken welke code 'aangeraakt' wordt en welke code niet, en daar wordt dan een rapportje over gegenereerd. Klinkt simpel, maar het is ontzettend handig! Het streven is uiteraard 100%: als je dat hebt, weet je zeker dat alle code in je implementatie gebruikt wordt in de verschillende testcases. Alles onder de 100% betekent dat er code is die niet getest wordt.
Maar... (er is altijd een 'maar' in mijn blogs) soms haal je die 100% niet en is het lastig te achterhalen waar dat dan door komt. Zo kwam ik vorige week het volgende tegen.
Ik had een stukje code dat een verbinding naar een ander systeem maakte. Mijn class heeft dus een property 'connected', die kun je op True of False zetten, met het verwachte resultaat. Ergens stond dus de volgende code:
public bool IsConnected
{
get
{
return _isConnected;
}
set
{
switch( value )
{
case true:
Connect();
break;
case false:
Disconnect();
break;
}
_isConnected = value;
}
}
Niet spannend. In mijn unit test heb ik keurig een test voor
IsConnected = true
en voor
IsConnected = false
, dus ik moet de 100% halen. Niet dus. Volgens de Code Coverage haalde ik in de set_IsConnected(bool) method een percentage van 85.71%. Goed, daar kan ik wel wat mee. De Code Coverage geeft keurig in rood aan welke code er niet geraakt wordt, dus dat zie je direct. Ik open mijn implementatie en wat zie ik? Geen rode balken. Alle code is aangeraakt. Toch krijg ik maar 85.71%, waar is de overige 14.29% gebleven? Ik ben 1 van de 7 code-blocks kwijt! Help!
Nou kon ik het hier bij laten. Ik heb immers gezien dat allebei de mogelijheden van mijn set_IsConnected getest zijn, dus ik had het er bij kunnen laten. Maar zo zit ik dus niet in elkaar. Ik moet weten hoe dat nou komt.
Ik ben in de IL code gedoken en zag de volgende regels staan:
.locals init ([0] bool CS$4$0000)
IL_0000: nop
IL_0001: ldarg.1
IL_0002: stloc.0
IL_0003: ldloc.0
IL_0004: switch (
IL_001c,
IL_0013)
IL_0011: br.s IL_0025
IL_0013: ldarg.0
IL_0014: call instance void ConsoleApplication4.SomeClass::Connect()
IL_0019: nop
IL_001a: br.s IL_0025
IL_001c: ldarg.0
IL_001d: call instance void ConsoleApplication4.SomeClass::Disconnect()
IL_0022: nop
IL_0023: br.s IL_0025
IL_0025: ldarg.0
IL_0026: ldarg.1
IL_0027: stfld bool ConsoleApplication4.SomeClass::_isConnected
IL_002c: ret
Laten we hier eens doorheen gaan en kijken wat we zien. (regelnummer 0 staat in IL_0000, nummer 1 in IL_0001 enz.)
0: doe niets
1: laad het argument (dat is dus de boolean 'value')
2: stop deze in een variabale 0
3: laadt wat er in variabele 0 zit (nog steeds de waarde van 'value')
4: de switch: hij maakt van de boolean een ordinal (dus false =0, true=1). Dit gebeurt ook bij enums, dan telt hij door. Dan kiest hij de switch behorende bij de ordinal, dus als de ordinal 0 is, springt hij naar de regel IL_001c, als hij 1 is dan gaat hij naar IL_0013.
Dus bij 0, met andere woorden: als 'value' == false, dan springt hij naar regel IL_001c.:
1c: laadt wat er in variabele 0 zit
1d: roep de method SomeClass::Disconnect() aan
22: nop, dus doe niets
23: spring naar IL_0025
25: vanaf hier staat de code die bij ons na de switch staat, dus het zetten van _isConnected
Als value 'true' is, dus de ordinal is 1, springt hij naar IL_0013:
13: laadt wat er in variabele 0 zit
14: roep de method SomeClass::Connect aan
19: nop
1a: spring naar IL_0025, met andere woorden: ga naar de code na onze switch.
Maar... we hebben nog niets gedaan met regel IL_0011. Daar staan een jump naar IL_0025 (dus na onze switch). Wat doet dat? Die regel wordt aangeroepen als hij een ordinal value heeft die groter is dan 1 (oftewel als een ordinal value heeft die groter is dan het aantal jumps in de switch in regel IL_0004). Dan springt hij automatisch naar de code na de switch. Dat is ook precies de bedoeling, want als hij een andere waarde vindt dan wat we in de 'case' hebben dan moet hij de switch overslaan.
Echter, in onze code testen we de twee mogelijke waardes true en false. Dus die jump in IL_0011 wordt nooit uitgevoerd. En daar zit nou net die 14% in. Die jump is de default die we niet in onze code hebben staan, maar die wel in de IL terecht komt maar nooit getest wordt!
Dat kunnen we uiteraard voorkomen door de code te wijzigen in:
switch (value)
{
case true:
Connect();
break;
default:
Disconnect();
break;
}
_isConnected = value;
Dan is de code ingevuld bij de default en hebben we opeens wel 100% coverage. Nou is dit niet zo duidelijk leesbaar als mijn eerste code, dus ik vind het niet zo'n verbeterinng. Even voor de puristen onder u: dit is in dit geval inderdaad beter:
set
{
if (value)
Connect();
else
Disconnect();
_isConnected = value;
}
Maar als je nou geen boolean hebt maar een enum, en je hebt alle mogelijkheden van die enum in je switch staan, dan zul je dus nooit 100% halen. Je moet dan een 'default' toevoegen, maar die kun je nooit raken want je kunt nou eenmaal geen andere waarde meegeven dan wat er in je enum staat.
set
{
switch (value)
{
case ConnectionState.Connected:
break;
case ConnectionState.Disconnected:
break;
}
}
Ook dit levert geen 100% op, en een default heeft hier geen zin (we hebben niet meer enums dan Connected en Disconnected). Ook hier kan ik een default gebruiken, maar ik wil graag zien wat ik doe en dus een duidelijke switch hebben.
Conclusie: als ik een code coverage van onder de 100% heb, kijk ik eerst even naar mijn switch statements....
PS Als je switched over strings, dan maakt hij er in de IL geen 'switch' van, maar een aantal if statements, hoewel ook hier de 'default' komt dus ook dan heb je hetzelfde fenomeen. 't Is maar dat je het weet.