当大多数 class 缺失覆盖时,代码覆盖分析显示 100% 的覆盖
Code Coverage analysis shows 100% coverage when most of the class is missing coverage
我有一个 class 实现了 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);
}
}
然后我为 class 创建了一系列单元测试。
[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 年 运行 单元测试覆盖率分析时,它告诉我 class 被单元测试覆盖了 100%。但是,我只测试了构造函数和 Start()
方法。 None 个单元测试涉及 Stop()
、StartAsync()
或 Dispose()
方法。
为什么 Visual Studio 告诉我我的代码覆盖率为 100%?
更新
我打开覆盖高亮,发现Stop()
方法没有被覆盖(如果我看了this right)。
有趣的是,分析告诉我它已被 100% 覆盖,尽管覆盖重点显示它未包含在任何单元测试路径中。
嗯,
- 内部 Dispose 可以在终结过程中调用,所以它可能会发生。
- Stop()可以通过PerformTimerCancellationCheck->RunTimer->Start->调用Start_sets_is_running
- StartAsync 显然没有调用。
考虑到各种 optimizations and code generation,检测代码覆盖率并非易事。我可能会容忍分析器出现 10%-20% 的错误,并且更愿意专注于检查 class 是否真的有效。代码覆盖率实际上表明该块是 "visited" 并不是其中的所有内容都按预期工作。但是当然,如果根本没有访问该块,那就是问题所在。
解释代码覆盖率工作方式的最简单方法如下:
- 从目标目录中取出所有dll,并根据
IL code
. 构建有向图G
- 使用
G
并创建所有可能的定向路径。
- 执行测试并标记相关路径。
- 计算百分比。
具有 100% 代码覆盖率的方法意味着您在 UT
的执行期间遍历所有方法的路径。(基本上该工具不知道哪个 class 在测试中UT
)
根据以上描述,您遇到的 CC 行为可能至少来自以下选项之一:
- 代码覆盖工具中的错误
- 当我的 CC 工具处理我的 dll 的不同版本时,我遇到了类似的问题。(
Rebuild Solution
在这种情况下解决了这个问题)
- 至少有一项测试直接调用这些方法。
- 您的至少一个测试间接调用了这些方法:通过继承、组合
等等...
要了解我提供您关注的原因:
如果您想查看特定测试的覆盖率 class 非常简单:
- 在"Test Explorer"右击->分组方式->Class
- Select 您要监控的 class 个。
- 右键单击 -> 分析 Selected 测试的代码覆盖率。
我有一个 class 实现了 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);
}
}
然后我为 class 创建了一系列单元测试。
[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 年 运行 单元测试覆盖率分析时,它告诉我 class 被单元测试覆盖了 100%。但是,我只测试了构造函数和 Start()
方法。 None 个单元测试涉及 Stop()
、StartAsync()
或 Dispose()
方法。
为什么 Visual Studio 告诉我我的代码覆盖率为 100%?
更新
我打开覆盖高亮,发现Stop()
方法没有被覆盖(如果我看了this right)。
有趣的是,分析告诉我它已被 100% 覆盖,尽管覆盖重点显示它未包含在任何单元测试路径中。
嗯,
- 内部 Dispose 可以在终结过程中调用,所以它可能会发生。
- Stop()可以通过PerformTimerCancellationCheck->RunTimer->Start->调用Start_sets_is_running
- StartAsync 显然没有调用。
考虑到各种 optimizations and code generation,检测代码覆盖率并非易事。我可能会容忍分析器出现 10%-20% 的错误,并且更愿意专注于检查 class 是否真的有效。代码覆盖率实际上表明该块是 "visited" 并不是其中的所有内容都按预期工作。但是当然,如果根本没有访问该块,那就是问题所在。
解释代码覆盖率工作方式的最简单方法如下:
- 从目标目录中取出所有dll,并根据
IL code
. 构建有向图 - 使用
G
并创建所有可能的定向路径。 - 执行测试并标记相关路径。
- 计算百分比。
G
具有 100% 代码覆盖率的方法意味着您在 UT
的执行期间遍历所有方法的路径。(基本上该工具不知道哪个 class 在测试中UT
)
根据以上描述,您遇到的 CC 行为可能至少来自以下选项之一:
- 代码覆盖工具中的错误
- 当我的 CC 工具处理我的 dll 的不同版本时,我遇到了类似的问题。(
Rebuild Solution
在这种情况下解决了这个问题) - 至少有一项测试直接调用这些方法。
- 您的至少一个测试间接调用了这些方法:通过继承、组合 等等...
要了解我提供您关注的原因:
如果您想查看特定测试的覆盖率 class 非常简单:
- 在"Test Explorer"右击->分组方式->Class
- Select 您要监控的 class 个。
- 右键单击 -> 分析 Selected 测试的代码覆盖率。