I’m a fan of code coverage as a way to ensure that there are covering tests. One area that I tend to rely heavily on Code Coverage for is to catch any tests that are no longer working correctly due to changes in the production code. That often works out well, but today I got betrayed by the code coverage engine.

The code that I worked on contained an if statement with a multi-step && expression.

bool IsAllWrong(int importantValue, bool b)
  bool a = importantValue == GetAnswer();
  bool c = false;
  bool d = false;
  if (!a && !b && !c && !d)
    return true;
  return false;

Of course I had tests that made the evaluation fail both because of importantValue and b. So what happend later was that GetAnswer() was updated, without the test for when importantValue being updated. Of course (my bad) that test had set b to true, causing the evaluation to fail on b, causing true to be returned. So the test passed, but not due to the thing I wanted to test. In a complex application, this is bound to happen every now and then. But usually, the code coverage scores will reveal that there is an execution path not covered. But not this time! The trustworthy code coverage analysis betrayed me!

To understand why I was betrayed by code coverage, we’ll have to look at the IL code generated. It looks like Visual Studios’ code coverage doesn’t check branch coverage, but rather just IL instruction coverage. In most cases that’s the same, but not this time.

// Initialize a, b, c & d
IL_0000:  nop         
IL_0001:  ldarg.1     
IL_0002:  ldarg.0     
IL_0003:  call        UserQuery.GetAnswer
IL_0008:  ceq         
IL_000A:  stloc.0     // a
IL_000B:  ldc.i4.0    
IL_000C:  stloc.1     // c
IL_000D:  ldc.i4.0    
IL_000E:  stloc.2     // d
// Evaluate conditional expression
IL_000F:  ldloc.0     // a
IL_0010:  brtrue.s    IL_001E
IL_0012:  ldarg.2     // b
IL_0013:  brtrue.s    IL_001E
IL_0015:  ldloc.1     // c
IL_0016:  brtrue.s    IL_001E
IL_0018:  ldloc.2     // d
IL_0019:  ldc.i4.0    
IL_001A:  ceq         
IL_001C:  br.s        IL_001F
IL_001E:  ldc.i4.0    // Run if expression was short circuited by a, b or c being true.
IL_001F:  stloc.3     
IL_0020:  ldloc.3     
IL_0021:  brfalse.s   IL_0029
IL_0023:  nop         
IL_0024:  ldc.i4.1    
IL_0025:  stloc.s     04 
IL_0027:  br.s        IL_002E
IL_0029:  ldc.i4.0    
IL_002A:  stloc.s     04 
IL_002C:  br.s        IL_002E
IL_002E:  ldloc.s     04 
IL_0030:  ret

We can see that the evaluation of the conditional expression takes the form of loading each of a, b and c and then short-circuiting the evaluation if the variable is true. The problem is that if either of a, b or c is true the branch instruction points to the same target: IL_001E. So to get all IL instructions run it is sufficient to test for two cases:

  • a=b=c=d=false
  • a=true; b=c=d=false

I still had both those cases covered. And because I was sloppy and passed in true for b when testing importantValue the unit test still passed.

So what I’ve learnt today is that:

  • Don’t rely on code coverage to accurately calculate branch coverage.
  • When writing tests for a complex if statement, it is risky to be sloppy with the parameters tested after the relevant one. Even though they are not relevant because of expression short circuiting when everything works, bad values can make the test pass on the wrong premises.
Posted in C# on 2017-04-06 | Tagged Code Coverage, Test Driven Development, Unit Tests

Source link

Leave a Reply

Your email address will not be published. Required fields are marked *