1

我有以下方法:此方法可以达到100%的代码覆盖率吗?

public static async Task<bool> CreateIfNotExistsAsync(
    this ISegment segment, 
    CancellationToken cancellationToken) 
{ 
    Requires.ArgNotNull(segment, nameof(segment)); 

    try 
    { 
     await segment.CreateAsync(cancellationToken); 
     return true; 
    } 
    catch (Exception error) 
    { 
     if (error.CanSuppress() && !cancellationToken.IsCancellationRequested) 
     { 
      var status = await segment.GetStatusAsync(cancellationToken); 
      if (status.Exists) 
      { 
       return false; 
      } 
     } 

     throw; 
    } 
} 

......为此我写了应涵盖所有块的测试。然而;代码覆盖率结果(在Visual Studio 2015年更新3)表明,两个块不包括:

Two blocks not covered

我想这有一些东西需要使用为await一个catch块内生成的代码,所以我尝试改写这样的方法:

public static async Task<bool> CreateIfNotExistsAsync(
    this ISegment segment, 
    CancellationToken cancellationToken) 
{ 
    Requires.ArgNotNull(segment, nameof(segment)); 

    ExceptionDispatchInfo capturedError; 

    try 
    { 
     await segment.CreateAsync(cancellationToken); 
     return true; 
    } 
    catch (Exception error) 
    { 
     if (error.CanSuppress() && !cancellationToken.IsCancellationRequested) 
     { 
      capturedError = ExceptionDispatchInfo.Capture(error); 
     } 
     else 
     { 
      throw; 
     } 
    } 

    var status = await segment.GetStatusAsync(cancellationToken); 
    if (!status.Exists) 
    { 
     capturedError.Throw(); 
    } 

    return false; 
} 

然而,仍然存在一个块未涵盖:

Still not fully covered

重写此方法是否可以完全覆盖?


这里是我的相关测试:

[TestMethod] 
public async Task Create_if_not_exists_returns_true_when_create_succeed() 
{ 
    var mock = new Mock<ISegment>(); 
    Assert.IsTrue(await mock.Object.CreateIfNotExistsAsync(default(CancellationToken))); 
    mock.Verify(_ => _.CreateAsync(It.IsAny<CancellationToken>()), Times.Once); 
} 

[TestMethod] 
public async Task Create_if_not_exists_throws_when_create_throws_and_cancellation_is_requested() 
{ 
    var mock = new Mock<ISegment>(); 
    var exception = new Exception(); 

    mock.Setup(_ => _.CreateAsync(It.IsAny<CancellationToken>())).Throws(exception); 

    try 
    { 
     await mock.Object.CreateIfNotExistsAsync(new CancellationToken(true)); 
     Assert.Fail(); 
    } 
    catch (Exception caught) 
    { 
     Assert.AreSame(exception, caught); 
    } 

    mock.Verify(_ => _.CreateAsync(It.IsAny<CancellationToken>()), Times.Once); 
    mock.Verify(_ => _.GetStatusAsync(It.IsAny<CancellationToken>()), Times.Never); 
} 

[TestMethod] 
public async Task Create_if_not_exists_throws_when_create_throws_non_suppressable_exception() 
{ 
    var mock = new Mock<ISegment>(); 
    var exception = new OutOfMemoryException(); 

    mock.Setup(_ => _.CreateAsync(It.IsAny<CancellationToken>())).Throws(exception); 

    try 
    { 
     await mock.Object.CreateIfNotExistsAsync(default(CancellationToken)); 
     Assert.Fail(); 
    } 
    catch (Exception caught) 
    { 
     Assert.AreSame(exception, caught); 
    } 

    mock.Verify(_ => _.CreateAsync(It.IsAny<CancellationToken>()), Times.Once); 
    mock.Verify(_ => _.GetStatusAsync(It.IsAny<CancellationToken>()), Times.Never); 
} 

[TestMethod] 
public async Task Create_if_not_exists_throws_when_create_throws_and_status_says_segment_doesnt_exists() 
{ 
    var mock = new Mock<ISegment>(); 
    var exception = new Exception(); 

    mock.Setup(_ => _.CreateAsync(It.IsAny<CancellationToken>())).Throws(exception); 
    mock.Setup(_ => _.GetStatusAsync(It.IsAny<CancellationToken>())) 
     .ReturnsAsync(new SegmentStatus(false, false, null, 0)); 

    try 
    { 
     await mock.Object.CreateIfNotExistsAsync(default(CancellationToken)); 
     Assert.Fail(); 
    } 
    catch (Exception caught) 
    { 
     Assert.AreSame(exception, caught); 
    } 

    mock.Verify(_ => _.CreateAsync(It.IsAny<CancellationToken>()), Times.Once); 
    mock.Verify(_ => _.GetStatusAsync(It.IsAny<CancellationToken>()), Times.Once); 
} 

[TestMethod] 
public async Task Create_if_not_exists_returns_false_when_create_throws_and_status_says_segment_exists() 
{ 
    var mock = new Mock<ISegment>(); 

    mock.Setup(_ => _.CreateAsync(It.IsAny<CancellationToken>())).Throws<Exception>(); 
    mock.Setup(_ => _.GetStatusAsync(It.IsAny<CancellationToken>())) 
     .ReturnsAsync(new SegmentStatus(true, false, null, 0)); 

    Assert.IsFalse(await mock.Object.CreateIfNotExistsAsync(default(CancellationToken))); 

    mock.Verify(_ => _.CreateAsync(It.IsAny<CancellationToken>()), Times.Once); 
    mock.Verify(_ => _.GetStatusAsync(It.IsAny<CancellationToken>()), Times.Once); 
} 

这是CanSuppress编辑逻辑:

private static readonly Exception[] EmptyArray = new Exception[0]; 

/// <summary> 
///  Determines whether an <see cref="Exception"/> can be suppressed. 
/// </summary> 
/// <param name="exception"> 
///  The <see cref="Exception"/> to test. 
/// </param> 
/// <returns> 
///  <c>true</c> when <paramref name="exception"/> can be suppressed; otherwise <c>false</c>. 
/// </returns> 
/// <remarks> 
/// <para> 
///  We do not want to suppress <see cref="OutOfMemoryException"/> or <see cref="ThreadAbortException"/> 
///  or any exception derived from them (except for <see cref="InsufficientMemoryException"/>, which we 
///  do allow suppression of). 
/// </para> 
/// <para> 
///  An exception that is otherwise suppressable is not considered suppressable when it has a nested 
///  non-suppressable exception. 
/// </para> 
/// </remarks> 
public static bool CanSuppress(this Exception exception) 
{ 
    foreach (var e in exception.DescendantsAndSelf()) 
    { 
     if ((e is OutOfMemoryException && !(e is InsufficientMemoryException)) || 
      e is ThreadAbortException) 
     { 
      return false; 
     } 
    } 

    return true; 
} 

private static IEnumerable<Exception> DescendantsAndSelf(this Exception exception) 
{ 
    if (exception != null) 
    { 
     yield return exception; 

     foreach (var child in exception.Children().SelectMany(ExceptionExtensions.DescendantsAndSelf)) 
     { 
      yield return child; 
     } 
    } 
} 

private static IEnumerable<Exception> Children(this Exception parent) 
{ 
    DebugAssumes.ArgNotNull(parent, nameof(parent)); 

    var aggregate = parent as AggregateException; 

    if (aggregate != null) 
    { 
     return aggregate.InnerExceptions; 
    } 
    else if (parent.InnerException != null) 
    { 
     return new[] { parent.InnerException }; 
    } 
    else 
    { 
     return ExceptionExtensions.EmptyArray; 
    } 
} 
+1

在您的第一张图片中,哪些区块未被覆盖..? – Rob

+0

我猜蓝色意味着覆盖;橙色意味着没有涵盖;紫色部分覆盖。所以我会说'抛出;'和'}'尽管'throw' **被覆盖,并且'}'甚至没有被覆盖的代码... –

+0

'}'不是'可包含的',我猜它没有被突出显示,因为代码永远不会超出该范围(这很好 - 同样的事情发生在返回语句之后)。至于投掷,这与结构无关,这是因为异常是可以抑制的,任务没有被取消。所以,你需要编写一个新的测试(使用原始代码),导致一个不可压制的异常,或者取消任务 – Rob

回答

2

好了 - 一个巨大的挖掘量之后 - 罪魁祸首是await segment.GetStatusAsync(cancellationToken) 。这会导致代码覆盖率查看throw,因为部分被覆盖。用非异步方法替换该行正确显示throw被覆盖

现在,async在内部创建一个状态机。你可以在代码覆盖率看到这一点,它发现命名

<CreateIfNotExistsAsync>d__1.MoveNext:

其中IL产生的方法,这蹦出来对我说:

IL_01B4: ldfld  MyExtensions+<CreateIfNotExistsAsync>d__1.<>s__1 
IL_01B9: isinst  System.Exception 
IL_01BE: stloc.s  0A 
IL_01C0: ldloc.s  0A 
IL_01C2: brtrue.s IL_01CB 
IL_01C4: ldarg.0  
IL_01C5: ldfld  MyExtensions+<CreateIfNotExistsAsync>d__1.<>s__1 
IL_01CA: throw  
IL_01CB: ldloc.s  0A 
IL_01CD: call  System.Runtime.ExceptionServices.ExceptionDispatchInfo.Capture 
IL_01D2: callvirt System.Runtime.ExceptionServices.ExceptionDispatchInfo.Throw 

这里,有两种方式抛出异常:

IL_01B4: ldfld  MyExtensions+<CreateIfNotExistsAsync>d__1.<>s__1 
IL_01B9: isinst  System.Exception 
IL_01BE: stloc.s  0A 
IL_01C0: ldloc.s  0A 
IL_01C2: brtrue.s IL_01CB 
IL_01C4: ldarg.0  
IL_01C5: ldfld  MyExtensions+<CreateIfNotExistsAsync>d__1.<>s__1 
IL_01CA: throw  

这实质上是从s__1以及检查它是否为Exception类型。例如:machine.state1 is Exception

然后,如果这是真的,它转移到IL_01CB

IL_01CB: ldloc.s  0A 
IL_01CD: call  System.Runtime.ExceptionServices.ExceptionDispatchInfo.Capture 
IL_01D2: callvirt System.Runtime.ExceptionServices.ExceptionDispatchInfo.Throw 

会抛出异常。但是,如果它是false,它将调用throw OpCode。

这意味着throw被翻译成两个可能的路径,其中只有一个被执行。我不确定在C#中IL_01B9: isinst System.Exception是否有可能是错误的,但我可能是错的 - 或者它可能在。NET一般。

坏消息是,我没有解决方案。我的建议是:使用代码覆盖作为准则,因为即使100%的覆盖率也不代表代码没有错误。话虽如此,你可以从逻辑上推断出throw被标记为“部分覆盖”,基本上与完全覆盖相同

+0

谢谢你的努力!我仍然希望找到一种方法让Visual Studio报告完全覆盖的代码 - 因为它确实是这样。 –

+0

@MårtenWikström我只能推荐另一个工具 - 例如'dotCover' - 它分析执行的代码行,而不是执行IL。我不确定内置覆盖是否可以用这种方式配置。奇怪的是,IL *对我来说似乎是多余的,因为它先前检查对象是一个异常,尽管即使在优化编译时仍然保持功能 – Rob