dotNed

Welkom bij dotNed Inloggen | Aanmelden | Help
in Zoeken

Dennis' avonturen in .net

Code coverage naar 100% (oftewel: hoe werkt een switch in IL?)

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.

Published Wednesday, April 12, 2006 5:20 PM door dvroegop
Filed Under:

Comments

 

mterwoord said:

Even ter informatie:

Er zijn ook andere code coverage check tools. 1 daarvan is NCover van ncover.sf.net, en die heeft dit probleem niet.
April 14, 2006 1:41 PM
 

Jeroen said:

Ik weet niet of het nou echt zo verkeerd is van de code coverage om in deze situatie een potentieel coverage probleem te noemen.

Die enum kan in de toekomst veranderen, waardoor de default wel relevant wordt. Dit is vooral denkbaar in een situatie waarin je switched over een enum die in een andere assembly gedefinieerd is en die de plaats van de assembly waar je buildtime mee werkt kan vervangen (de eindgebruiker kan dit tenslotte via policy doen).

Vanuit dat oogpunt kun je redeneren dat de code coverage je erop heeft gewezen dat je bij een switch over een enum altijd een default op moet nemen. Bij een boolean is het echter een beetje suf (alhoewel, nullable? :))
April 18, 2006 11:18 AM
 

dvroegop said:

Tja, bij een bool slaat het helemaal nergens op. (bool? is een ander type dan bool, dus daar hebben we het maar niet over).

Dat van die mogelijke toekomstige uitbreidingen had ik ook al bedacht, het is dan ook een guideline van Microsoft om altijd een 'unknown' of zo iets op te nemen in je enum. Op die manier kun je wel een default opnemen en een fout genereren bijvoorbeeld. Maar neem nou het volgende:
public enum DaysOfWeek {monday, tuesday, wednesday, thursday, friday, saturday, sunday};
Moet hier nou een 'unknown' bij? Dat slaat nergens op: ik verwacht niet dat een dezer dagen een nieuwe weekdag uitgevonden wordt (nou ja, als hij bij het weekend gaat horen heb ik er geen moeite mee :)

Ook je switch wordt gek: voor maandag t/m zaterdag een case, voor zondag de default? Dat staat raar.

Maar goed, de kern van mijn verhaal was: als je geen 100% haalt, kan dat door dit soort dingen komen en het loont de moeite om daar eens naar te gaan kijken. Er zijn nog veel meer voorbeelden te verzinnen!
April 18, 2006 9:46 PM
 

Jeroen said:

(Beetje late reactie maarja)

Goede punten! Vooral bij fixed enums zoals weekdays is het een beetje silly inderdaad.
April 27, 2006 1:55 PM
 

XIU’s Blog » Blog Archive » links for 2006-05-09 said:

May 9, 2006 2:39 AM
 

peSHIr said:

Mmm, eigenlijk vind ik dit wel goed.

Vanuit defensive programming schrijf ik namelijk in principe geen switch op een enum zonder een default. Al is het maar met een throw new Exception("Huh") erin, maar dan met een wat zinnigere melding.

Als het goed is kom je daar nooit (tenzij je dat test voor je 100% coverage ;-)), maar als er ooit voor wat voor reden een waarde aan de definitie van die enum wordt toegevoegd zal je code nooit geruisloos deze waarde overslaan en dan maar niks doen. Toen de code werd geschreven kon default niet afgaan omdat er geen andere enum values bekend waren. Maak die aanname dus vooral lekker impliciet.

Andere reden: je kunt zonder problemen elke willekeurige integer casten naar een enum (gedefinieerd op basis van een integer). Dus zelfs als er geen enumerable value bestaat, kan je switch toch in de default komen!

Maar goed. Bedankt voor nuttige info, dat default (zelfs als hij niet expliciet in je switch code staat) meetelt in coverage.
March 26, 2009 3:55 PM
Anonymous comments are disabled

About dvroegop

Programmeert al sinds 1982. Microsoft Surface MVP.
Powered by Community Server, by Telligent Systems