Code coverage: partially covered, but WHY?
-
Dear all, I have been working on unit test in C# and found out the following line is always indicated as partially covered. ClassA *objectA = new ClassA(0); ClassA has two constructors: ClassA::ClassA() ClassA::ClassA(int param) Several notes: 1. ClassA is defined in native C++ 2. Test code is written in managed C++ 3. Code coverage analysis shows the inside of constructor ClassA::ClassA(int param) is fully covered. What do you guys think of this? Is it because that the new operation can throw some exception and we have not tested the case yet? Any ideas/comments will be appreciated. Thanks
Hao
-
Dear all, I have been working on unit test in C# and found out the following line is always indicated as partially covered. ClassA *objectA = new ClassA(0); ClassA has two constructors: ClassA::ClassA() ClassA::ClassA(int param) Several notes: 1. ClassA is defined in native C++ 2. Test code is written in managed C++ 3. Code coverage analysis shows the inside of constructor ClassA::ClassA(int param) is fully covered. What do you guys think of this? Is it because that the new operation can throw some exception and we have not tested the case yet? Any ideas/comments will be appreciated. Thanks
Hao
If you put that line in a static method, compile it and then disassemble it you'll see that there is branching to account for the possibility that
new
fails. Also, note that the constructor call is wrapped in a try-fault in order to free the memory allocated for the unmanaged object in case the constructor fails:.method public hidebysig static void Test() cil managed
{
// Code size 30 (0x1e)
.maxstack 2
.locals ([0] valuetype ClassA* V_0)
IL_0000: ldc.i4.1
IL_0001: call void* modopt([mscorlib]System.Runtime.CompilerServices.CallConvCdecl) new(uint32)
IL_0006: stloc.0
.try
{
IL_0007: ldloc.0
IL_0008: brtrue.s IL_000c
IL_000a: leave.s IL_001d
IL_000c: ldloc.0
IL_000d: ldc.i4.0
IL_000e: call valuetype ClassA* modopt([mscorlib]System.Runtime.CompilerServices.CallConvThiscall) 'ClassA.{ctor}'(valuetype ClassA* modopt([mscorlib]System.Runtime.CompilerServices.IsConst) modopt([mscorlib]System.Runtime.CompilerServices.IsConst),
int32)
IL_0013: pop
IL_0014: leave.s IL_001d
} // end .try
fault
{
IL_0016: ldloc.0
IL_0017: call void modopt([mscorlib]System.Runtime.CompilerServices.CallConvCdecl) delete(void*)
IL_001c: endfinally
} // end handler
IL_001d: ret
} // end of method Class1::TestIf ClassA were managed the compiled code would be a single (and much simpler) block:
.method public hidebysig static void Test2() cil managed
{
// Code size 8 (0x8)
.maxstack 1
IL_0000: ldc.i4.0
IL_0001: newobj instance void ClassLibrary1.Class1::.ctor(int32)
IL_0006: pop
IL_0007: ret
} // end of method Class1::Test2Best regards Dennis
-
If you put that line in a static method, compile it and then disassemble it you'll see that there is branching to account for the possibility that
new
fails. Also, note that the constructor call is wrapped in a try-fault in order to free the memory allocated for the unmanaged object in case the constructor fails:.method public hidebysig static void Test() cil managed
{
// Code size 30 (0x1e)
.maxstack 2
.locals ([0] valuetype ClassA* V_0)
IL_0000: ldc.i4.1
IL_0001: call void* modopt([mscorlib]System.Runtime.CompilerServices.CallConvCdecl) new(uint32)
IL_0006: stloc.0
.try
{
IL_0007: ldloc.0
IL_0008: brtrue.s IL_000c
IL_000a: leave.s IL_001d
IL_000c: ldloc.0
IL_000d: ldc.i4.0
IL_000e: call valuetype ClassA* modopt([mscorlib]System.Runtime.CompilerServices.CallConvThiscall) 'ClassA.{ctor}'(valuetype ClassA* modopt([mscorlib]System.Runtime.CompilerServices.IsConst) modopt([mscorlib]System.Runtime.CompilerServices.IsConst),
int32)
IL_0013: pop
IL_0014: leave.s IL_001d
} // end .try
fault
{
IL_0016: ldloc.0
IL_0017: call void modopt([mscorlib]System.Runtime.CompilerServices.CallConvCdecl) delete(void*)
IL_001c: endfinally
} // end handler
IL_001d: ret
} // end of method Class1::TestIf ClassA were managed the compiled code would be a single (and much simpler) block:
.method public hidebysig static void Test2() cil managed
{
// Code size 8 (0x8)
.maxstack 1
IL_0000: ldc.i4.0
IL_0001: newobj instance void ClassLibrary1.Class1::.ctor(int32)
IL_0006: pop
IL_0007: ret
} // end of method Class1::Test2Best regards Dennis
Thanks Dennis, That's very thorough. Then, I guess there is no way we can get it fully covered without making it managed. Hao
-
Thanks Dennis, That's very thorough. Then, I guess there is no way we can get it fully covered without making it managed. Hao
Actually, there is. There are many fault injection[^] techniques and tools to cover error handling code paths that are difficult to execute as part of normal control flow. The real question is whether or not it's worth it. Regarding block coverage in particular, it's okay not to reach 100%. In fact, in my experience real-world code bases virtually always have code paths that are difficult to hit with automation and the benefits of automating may not outweigh the cost, e.g. when automating a particular scenario is very difficult to automate and the code path rarely changes or when making the automated test reliable proves difficult. There are alternatives though. In simple cases a code review or inspection may be enough. Or you may be able to get coverage through specialized testing such as stress or fuzz testing (which should be part of the overall test plan anyway). I suggest you consider this before making a final call. I would certainly not recommend porting unmanaged to managed code if the only reason is to increase block coverage a bit (assuming there was a good reason to mix managed and unmanaged code in the first place).
Best regards D.C.
-
Thanks Dennis, That's very thorough. Then, I guess there is no way we can get it fully covered without making it managed. Hao
Hao from MSFT wrote:
Then, I guess there is no way we can get it fully covered without making it managed.
Since when is reaching 100% coverage more important than functionality? If unmanaged code is dropped that easily, then I'm wondering about the argumentation on using unmanaged code in the first place.
Bastard Programmer from Hell :suss: If you can't read my code, try converting it here[^]