最近的项目中单体测试时遇到了关于可空类型的一些疑惑,具体问题是这样的:
在一个方法体内部有一些对可空类型的赋值操作,当对这个方法进行单体测试时,有时仅仅进行非空值的赋值测试,即可是覆盖率达到100%,但有时却必须进行非空值和空值的测试之后才能使覆盖率达到100%。到底为什么会出现这个问题呢?是不是编译器自动给一些方法加上了一些if等的条件分歧操作呢?
让我们来看个简单的例子吧。
在这里有两个方法,分别接受decimal的非空和可空版本来对一个非空或者可空版本来进行赋值。
public void test1(decimal n)
{
decimal m;
m = n;
}
public void test2(decimal? n)
{
decimal? m;
m = n;
}
具体的IL如下
.method public hidebysig instance void test1(valuetype [mscorlib]System.Decimal n) cil managed
{
// コード サイズ 4 (0x4)
.maxstack 1
.locals init (valuetype [mscorlib]System.Decimal V_0)
IL_0000: nop
IL_0001: ldarg.1
IL_0002: stloc.0
IL_0003: ret
} // end of method a::test1
.method public hidebysig instance void test2(valuetype [mscorlib]System.Nullable`1<valuetype [mscorlib]System.Decimal> n) cil managed
{
// コード サイズ 4 (0x4)
.maxstack 1
.locals init (valuetype [mscorlib]System.Nullable`1<valuetype [mscorlib]System.Decimal> V_0)
IL_0000: nop
IL_0001: ldarg.1
IL_0002: stloc.0
IL_0003: ret
} // end of method a::test2
由IL可见这两个方法都是初始化内部变量,然后将参数加加入堆栈(IL_001),然后将堆栈顶端的条目存入局部变量(IL_0002)。于是我大胆的推测,这时的test2的单体测试只要进行非空值或者空值的任意一个case就可以使覆盖率达到100%,同时测试的结果也验证了我的猜测的正确性。
那么到底什么情况下,对于可空类型的单体测试的必须进行可空值和空值的测试才能保证覆盖率呢?这时同事一句话让我又进行了下面的测试:当输入参数为非空版本时,只要进行非空值或者空值的测试就可以保证覆盖率。
例子如下:
public void test3(decimal n)
{
decimal? m;
m = n;
}
IL如下:
.method public hidebysig instance void test3(valuetype [mscorlib]System.Decimal n) cil managed
{
// コード サイズ 11 (0xb)
.maxstack 2
.locals init (valuetype [mscorlib]System.Nullable`1<valuetype [mscorlib]System.Decimal> V_0)
IL_0000: nop
IL_0001: ldloca.s V_0
IL_0003: ldarg.1
IL_0004: call instance void valuetype [mscorlib]System.Nullable`1<valuetype [mscorlib]System.Decimal>::.ctor(!0)
IL_0009: nop
IL_000a: ret
} // end of method a::test3
对比test2,我们发现test3发生了很大的变化。
但是单体测试仍然只要进行非空值或者空值的任意一个case就可以使覆盖率达到100%。
那到底为何在项目中出现了一些必须进行非空值和空值的测试才可能满足覆盖率的问题呢,重新对检查了项目和自己的测试例子的区别,发现实际的项目还存在类型的隐式转换,于是又进行了下面的测试用例:
public void test4(int n)
{
decimal? m;
m = n;
}
public void test5(int? n)
{
decimal? m;
m = n;
}
再来重新看一下IL
.method public hidebysig instance void test4(int32 n) cil managed
{
// コード サイズ 16 (0x10)
.maxstack 2
.locals init (valuetype [mscorlib]System.Nullable`1<valuetype [mscorlib]System.Decimal> V_0)
IL_0000: nop
IL_0001: ldloca.s V_0
IL_0003: ldarg.1
IL_0004: call valuetype [mscorlib]System.Decimal [mscorlib]System.Decimal::op_Implicit(int32)
IL_0009: call instance void valuetype [mscorlib]System.Nullable`1<valuetype [mscorlib]System.Decimal>::.ctor(!0)
IL_000e: nop
IL_000f: ret
} // end of method a::test4
.method public hidebysig instance void test5(valuetype [mscorlib]System.Nullable`1<int32> n) cil managed
{
// コード サイズ 42 (0x2a)
.maxstack 3
.locals init (valuetype [mscorlib]System.Nullable`1<valuetype [mscorlib]System.Decimal> V_0,
valuetype [mscorlib]System.Nullable`1<int32> V_1,
valuetype [mscorlib]System.Nullable`1<valuetype [mscorlib]System.Decimal> V_2)
IL_0000: nop
IL_0001: ldarg.1
IL_0002: stloc.1
IL_0003: ldloca.s V_1
IL_0005: call instance bool valuetype [mscorlib]System.Nullable`1<int32>::get_HasValue()
IL_000a: brtrue.s IL_0017
IL_000c: ldloca.s V_2
IL_000e: initobj valuetype [mscorlib]System.Nullable`1<valuetype [mscorlib]System.Decimal>
IL_0014: ldloc.2
IL_0015: br.s IL_0028
IL_0017: ldloca.s V_1
IL_0019: call instance !0 valuetype [mscorlib]System.Nullable`1<int32>::GetValueOrDefault()
IL_001e: call valuetype [mscorlib]System.Decimal [mscorlib]System.Decimal::op_Implicit(int32)
IL_0023: newobj instance void valuetype [mscorlib]System.Nullable`1<valuetype [mscorlib]System.Decimal>::.ctor(!0)
IL_0028: stloc.0
IL_0029: ret
} // end of method a::test5
这次让我眼睛一亮的是,在test5中出现了条件判断,这在我们的C#中是没有的,为什么会出现这判断呢。仔细分析IL,发现这个地方是就引出了Nullable<T>的一些特性。
详细地对test5进行分析,再IL中存在3个局部变量:两个Nullable< Decimal >和一个Nullable<int32>,这与之前的test1-test4是明显的不同的,这充分说明编译器为我们追加了两个局部变量的定义,
继续往下看,000a判断参数是否为空,如果不为空,则跳转到0017将局部变量V_1入栈。如果为空,则继续执行000c。
到这里我们就很明显的可以得到结论test5,必须进行非空值和空值的测试才可能满足覆盖率100%。
本文探讨了C#中可空类型在单元测试时的覆盖率问题,通过对比不同情况下的中间语言(IL)代码,揭示了何时及为何需要对非空值和空值进行测试以达到完全覆盖。
1027

被折叠的 条评论
为什么被折叠?



