代码覆盖率分析显示,当大多数班级缺少覆盖范围时,覆盖范围为100%

时间:2015-08-01 03:17:12

标签: c# unit-testing code-coverage visual-studio-2015

我有一个实现IDisposable接口的类。

using System;
using System.Threading;
using System.Threading.Tasks;

/// <summary>
/// <para>
/// The Engine Timer allows for starting a timer that will execute a callback at a given interval.
/// </para>
/// <para>
/// The timer may fire:
///  - infinitely at the given interval
///  - fire once
///  - fire _n_ number of times.
/// </para>
/// <para>
/// The Engine Timer will stop its self when it is disposed of.
/// </para>
/// <para>
/// The Timer requires you to provide it an instance that will have an operation performed against it.
/// The callback will be given the generic instance at each interval fired.
/// </para>
/// <para>
/// In the following example, the timer is given an instance of an IPlayer. 
/// It starts the timer off with a 30 second delay before firing the callback for the first time.
/// It tells the timer to fire every 60 seconds with 0 as the number of times to fire. When 0 is provided, it will run infinitely.
/// Lastly, it is given a callback, which will save the player every 60 seconds.
/// @code
/// var timer = new EngineTimer<IPlayer>(new DefaultPlayer());
/// timer.StartAsync(30000, 6000, 0, (player, timer) => player.Save());
/// @endcode
/// </para>
/// </summary>
/// <typeparam name="T">The type that will be provided when the timer callback is invoked.</typeparam>
public sealed class EngineTimer<T> : CancellationTokenSource, IDisposable
{
    /// <summary>
    /// The timer task
    /// </summary>
    private Task timerTask;

    /// <summary>
    /// How many times we have fired the timer thus far.
    /// </summary>
    private long fireCount = 0;

    /// <summary>
    /// Initializes a new instance of the <see cref="EngineTimer{T}"/> class.
    /// </summary>
    /// <param name="callback">The callback.</param>
    /// <param name="state">The state.</param>
    public EngineTimer(T state)
    {
        if (state == null)
        {
            throw new ArgumentNullException(nameof(state), "EngineTimer constructor requires a non-null argument.");
        }

        this.StateData = state;
    }

    /// <summary>
    /// Gets the object that was provided to the timer when it was instanced.
    /// This object will be provided to the callback at each interval when fired.
    /// </summary>
    public T StateData { get; private set; }

    /// <summary>
    /// Gets a value indicating whether the engine timer is currently running.
    /// </summary>
    public bool IsRunning { get; private set; }

    /// <summary>
    /// <para>
    /// Starts the timer, firing a synchronous callback at each interval specified until `numberOfFires` has been reached.
    /// If `numberOfFires` is 0, then the callback will be called indefinitely until the timer is manually stopped.
    /// </para>
    /// <para>
    /// The following example shows how to start a timer, providing it a callback.
    /// </para>
    /// @code
    /// var timer = new EngineTimer<IPlayer>(new DefaultPlayer());
    /// double startDelay = TimeSpan.FromSeconds(30).TotalMilliseconds;
    /// double interval = TimeSpan.FromMinutes(10).TotalMilliseconds;
    /// int numberOfFires = 0;
    /// 
    /// timer.Start(
    ///     startDelay, 
    ///     interval, 
    ///     numberOfFires, 
    ///     (player, timer) => player.Save());
    /// @endcode
    /// </summary>
    /// <param name="startDelay">
    /// <para>
    /// The `startDelay` is used to specify how much time must pass before the timer can invoke the callback for the first time.
    /// If 0 is provided, then the callback will be invoked immediately upon starting the timer.
    /// </para>
    /// <para>
    /// The `startDelay` is measured in milliseconds.
    /// </para>
    /// </param>
    /// <param name="interval">The interval in milliseconds.</param>
    /// <param name="numberOfFires">Specifies the number of times to invoke the timer callback when the interval is reached. Set to 0 for infinite.</param>
    public void Start(double startDelay, double interval, int numberOfFires, Action<T, EngineTimer<T>> callback)
    {
        this.IsRunning = true;

        this.timerTask = Task
            .Delay(TimeSpan.FromMilliseconds(startDelay), this.Token)
            .ContinueWith(
                (task, state) => RunTimer(task, (Tuple<Action<T, EngineTimer<T>>, T>)state, interval, numberOfFires),
                Tuple.Create(callback, this.StateData),
                CancellationToken.None,
                TaskContinuationOptions.ExecuteSynchronously | TaskContinuationOptions.OnlyOnRanToCompletion,
                TaskScheduler.Default);
    }

    /// <summary>
    /// Starts the specified start delay.
    /// </summary>
    /// <param name="startDelay">The start delay in milliseconds.</param>
    /// <param name="interval">The interval in milliseconds.</param>
    /// <param name="numberOfFires">Specifies the number of times to invoke the timer callback when the interval is reached. Set to 0 for infinite.</param>
    public void StartAsync(double startDelay, double interval, int numberOfFires, Func<T, EngineTimer<T>, Task> callback)
    {
        this.IsRunning = true;

        this.timerTask = Task
            .Delay(TimeSpan.FromMilliseconds(startDelay), this.Token)
            .ContinueWith(
                async (task, state) => await RunTimerAsync(task, (Tuple<Func<T, EngineTimer<T>, Task>, T>)state, interval, numberOfFires),
                Tuple.Create(callback, this.StateData),
                CancellationToken.None,
                TaskContinuationOptions.ExecuteSynchronously | TaskContinuationOptions.OnlyOnRanToCompletion,
                TaskScheduler.Default);
    }

    /// <summary>
    /// Stops the timer for this instance.
    /// Stopping the timer will not dispose of the EngineTimer, allowing you to restart the timer if you need to.
    /// </summary>
    public void Stop()
    {
        if (!this.IsCancellationRequested)
        {
            this.Cancel();
        } 
        this.IsRunning = false;
    }

    /// <summary>
    /// Stops the timer and releases the unmanaged resources used by the <see cref="T:System.Threading.CancellationTokenSource" /> class and optionally releases the managed resources.
    /// </summary>
    /// <param name="disposing">true to release both managed and unmanaged resources; false to release only unmanaged resources.</param>
    protected override void Dispose(bool disposing)
    {
        if (disposing)
        {
            this.IsRunning = false;
            this.Cancel();
        }

        base.Dispose(disposing);
    }

    private async Task RunTimer(Task task, Tuple<Action<T, EngineTimer<T>>, T> state, double interval, int numberOfFires)
    {
        while (!this.IsCancellationRequested)
        {
            // Only increment if we are supposed to.
            if (numberOfFires > 0)
            {
                this.fireCount++;
            }

            state.Item1(state.Item2, this);
            await PerformTimerCancellationCheck(interval, numberOfFires);
        }
    }

    private async Task RunTimerAsync(Task task, Tuple<Func<T, EngineTimer<T>, Task>, T> state, double interval, int numberOfFires)
    {
        while (!this.IsCancellationRequested)
        {
            // Only increment if we are supposed to.
            if (numberOfFires > 0)
            {
                this.fireCount++;
            }

            await state.Item1(state.Item2, this);
            await PerformTimerCancellationCheck(interval, numberOfFires);
        }
    }

    private async Task PerformTimerCancellationCheck(double interval, int numberOfFires)
    {
        // If we have reached our fire count, stop. If set to 0 then we fire until manually stopped.
        if (numberOfFires > 0 && this.fireCount >= numberOfFires)
        {
            this.Stop();
        }

        await Task.Delay(TimeSpan.FromMilliseconds(interval), this.Token).ConfigureAwait(false);
    }
}

然后,我为该课程创建了一系列单元测试。

[TestClass]
public class EngineTimerTests
{
    [TestMethod]
    [TestCategory("MudDesigner")]
    [TestCategory("Engine")]
    [TestCategory("Engine Core")]
    [Owner("Johnathon Sullinger")]
    [ExpectedException(typeof(ArgumentNullException))]
    public void Exception_thrown_with_null_ctor_argument()
    {
        // Act
        new EngineTimer<ComponentFixture>(null);
    }

    [TestMethod]
    [TestCategory("MudDesigner")]
    [TestCategory("Engine")]
    [TestCategory("Engine Core")]
    [Owner("Johnathon Sullinger")]
    public void Ctor_sets_state_property()
    {
        // Arrange
        var fixture = new ComponentFixture();

        // Act
        var engineTimer = new EngineTimer<ComponentFixture>(fixture);

        // Assert
        Assert.IsNotNull(engineTimer.StateData, "State was not assigned from the constructor.");
        Assert.AreEqual(fixture, engineTimer.StateData, "An incorrect State object was assigned to the timer.");
    }

    [TestMethod]
    [TestCategory("MudDesigner")]
    [TestCategory("Engine")]
    [TestCategory("Engine Core")]
    [Owner("Johnathon Sullinger")]
    public void Start_sets_is_running()
    {
        // Arrange
        var fixture = new ComponentFixture();
        var engineTimer = new EngineTimer<ComponentFixture>(fixture);

        // Act
        engineTimer.Start(0, 1, 0, (component, timer) => { });

        // Assert
        Assert.IsTrue(engineTimer.IsRunning, "Engine Timer was not started.");
    }

    [TestMethod]
    [TestCategory("MudDesigner")]
    [TestCategory("Engine")]
    [TestCategory("Engine Core")]
    [Owner("Johnathon Sullinger")]
    public void Callback_invoked_when_running()
    {
        // Arrange
        var fixture = new ComponentFixture();
        var engineTimer = new EngineTimer<ComponentFixture>(fixture);
        bool callbackInvoked = false;

        // Act
        engineTimer.Start(0, 1, 0, (component, timer) => { callbackInvoked = true; });
        Task.Delay(20);

        // Assert
        Assert.IsTrue(callbackInvoked, "Engine Timer did not invoke the callback as expected.");
    }
}

当我在Visual Studio 2015中运行单元测试覆盖率分析时,它告诉我该类是100%由单元测试覆盖。但是,我只测试了构造函数和Start()方法。所有单元测试均未触及Stop()StartAsync()Dispose()方法。

为什么Visual Studio会告诉我我的代码覆盖率为100%?

Code Coverage

更新

我启用了覆盖率突出显示并发现未涵盖Stop()方法(如果我阅读this right)。

Coverage

有趣的是,分析告诉我它已100%覆盖,即使覆盖亮点显示它未包含在任何单元测试路径中。

2 个答案:

答案 0 :(得分:1)

解释代码覆盖方式的最简单方法如下:

  1. 从目标目录获取所有dll,并根据G构建有向图IL code
  2. 使用G并创建所有可能的定向路径。
  3. 执行测试并标记相关路径。
  4. 计算百分比。
  5. 具有100%代码覆盖率的方法意味着您在执行UT期间遍历所有方法的路径。(基本上该工具不知道UT中的哪个类正在测试中)

    根据以上描述,您所遇到的CC行为可能至少来自以下一个选项:

    1. 代码覆盖率工具中的错误
    2. 当我的CC工具处理我的dll的不同版本时,我遇到了类似的问题。(Rebuild Solution在这种情况下解决了问题)
    3. 至少有一个测试直接调用这些方法。
    4. 至少有一个测试间接调用这些方法:通过继承,组合 等等...
    5. 了解我提供您遵循的原因:

      enter image description here

      如果您想查看特定测试类的覆盖范围,这很容易:

      1. 在“测试资源管理器”中右键单击 - &gt;分组依据 - &gt;类
      2. 选择您要监控的课程。
      3. 右键单击 - &gt;分析所选测试的代码覆盖率。

答案 1 :(得分:0)

那么,

  • 内部处理可以在最终确定期间调用,因此可能会发生。
  • Stop()可以通过PerformTimerCancellationCheck-&gt; RunTimer-&gt; Start-&gt;来调用。 Start_sets_is_running
  • StartAsync显然没有打电话。

考虑到各种optimizations and code generation,检测代码覆盖率并非易事。我可能会从分析仪中容忍10%-20%的误差,并且更愿意专注于检查该类是否真的有效。代码覆盖率实际上表明该块被访问了#34;并不是说它里面的一切都按预期工作。但是当然如果根本没有访问该块,那就是问题所在。