当大多数班级缺少覆盖时,代码覆盖率分析显示100%的覆盖率 [英] Code Coverage analysis shows 100% coverage when most of the class is missing coverage
问题描述
我有一个实现IDisposable接口的类。
使用系统;
使用System.Threading;
使用System.Threading.Tasks;
///< summary>
///< para>
///引擎计时器允许启动计时器,该计时器将以给定的时间间隔执行回调。
///< / para>
///< para>
///计时器可能会触发:
///-以给定的间隔无限触发
///-触发一次
///-触发_n_次。
///< / para>
///< para>
///引擎计时器在处理后将自行停止。
///< / para>
///< para>
///计时器要求您提供一个实例,该实例将对其执行操作。
///将在每次触发间隔为回调实例提供通用实例。
///< / para>
///< para>
///在下面的示例中,为计时器提供了IPlayer的实例。
///在第一次触发回调之前,它会延迟30秒启动计时器。
///告诉计时器每60秒触发一次,触发次数为0。如果提供0,它将无限运行。
///最后,将为其提供回调,这将每60秒保存一次播放器。
/// @code
/// var timer = new EngineTimer< IPlayer>(new DefaultPlayer());
/// timer.StartAsync(30000,6000,0,(player,timer)=> player.Save());
/// @endcode
///< / para>
///< / summary>
///< typeparam name = T>在调用计时器回调时将提供的类型。
个公共密封类EngineTimer< T> :CancellationTokenSource,IDisposable
{
///< summary>
///计时器任务
///< / summary>
private Task timerTask;
///< summary>
///到目前为止,我们已经触发计时器多少次了。
///< / summary>
private long fireCount = 0;
///< summary>
///初始化< see cref = EngineTimer {T} />的新实例。类。
///< / summary>
///< param name = callback>回调。< / param>
///< param name = state>状态。< / param>
public EngineTimer(T state)
{
if(state == null)
{
throw new ArgumentNullException(nameof(state), EngineTimer构造函数需要一个非-null参数。);
}
this.StateData =状态;
}
///< summary>
///获取实例化时提供给计时器的对象。
///在触发时,将在每个时间间隔将此对象提供给回调。
///< / summary>
public T StateData {get;私人套装; }
///< summary>
///获取一个值,该值指示引擎计时器当前是否正在运行。
///< / summary>
public bool IsRunning {get;私人套装; }
///< summary>
///< para>
///启动计时器,在指定的每个间隔触发一个同步回调,直到达到`numberOfFires`为止。
///如果`numberOfFires`为0,则将无限期调用回调,直到手动停止计时器为止。
///< / para>
///< para>
///下面的示例显示如何启动计时器,并为其提供回调。
///< / 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>
///`startDelay`用于指定计时器第一次调用回调之前必须经过多少时间。
///如果提供0,则在启动计时器后将立即调用回调。
///< / para>
///< para>
///`startDelay`以毫秒为单位。
///< / para>
///< / param>
///< param name = interval>时间间隔,以毫秒为单位。< / param>
///< param name = numberOfFires>指定到达间隔时调用计时器回调的次数。 < / param>设置为0表示无限。
public void Start(double startDelay,double interval,int numberOfFires,Action< T,EngineTimer< T>>回调)
{
this.IsRunning = true;
this.timerTask =任务
.Delay(TimeSpan.FromMilliseconds(startDelay),this.Token)
.ContinueWith(
(task,state)=> RunTimer(任务,(Tuple< Action< T,EngineTimer< T>),状态,间隔,numberOfFires),
Tuple.Create(回调,this.StateData),
CancellationToken.None ,
TaskContinuationOptions.ExecuteSynchronously | TaskContinuationOptions.OnlyOnRanToCompletion,
TaskScheduler.Default);,
}
///< summary>
///开始指定的启动延迟。
///< / summary>
///< param name = startDelay>启动延迟,以毫秒为单位。< / param>
///< param name = interval>时间间隔,以毫秒为单位。< / param>
///< param name = numberOfFires>指定到达间隔时调用计时器回调的次数。 < / param>设置为0表示无限。
public void StartAsync(double startDelay,double interval,int numberOfFires,Func< T,EngineTimer< T> ;, Task>回调))
{
this.IsRunning = true;
this.timerTask =任务
.Delay(TimeSpan.FromMilliseconds(startDelay),this.Token)
.ContinueWith(
async(task,state)=> ;等待RunTimerAsync(task,(Tuple< Func< T,EngineTimer< T> Task>)状态,间隔,numberOfFires),
Tuple.Create(回调,this.StateData),
CancellationToken.None,
TaskContinuationOptions.ExecuteSynchronously | TaskContinuationOptions.OnlyOnRanToCompletion,
TaskScheduler.Default);
}
///< summary>
///停止该实例的计时器。
///停止计时器不会丢弃EngineTimer,允许您在需要时重新启动计时器。
///< / summary>
public void Stop()
{
if(!this.IsCancellationRequested)
{
this.Cancel();
}
this.IsRunning = false;
}
///< summary>
///停止计时器并释放< see cref = T:System.Threading.CancellationTokenSource />使用的非托管资源。类,并有选择地释放托管资源。
///< / summary>
///< param name = disposed> true,以同时释放托管资源和非托管资源;如果为false,则仅释放非托管资源。< / param>
保护的覆盖无效void Dispose(布尔处理)
{
if(处理)
{
this.IsRunning = false;
this.Cancel();
}
base.Dispose(处置);
}
私有异步任务RunTimer(任务任务,Tuple< Action< T,EngineTimer< T>>,T>状态,双间隔,int numberOfFires)
{
while(!this.IsCancellationRequested)
{
//仅在应有的情况下增加。
if(numberOfFires> 0)
{
this.fireCount ++;
}
state.Item1(state.Item2,此);
等待PerformTimerCancellationCheck(interval,numberOfFires);
}
}
私有异步任务RunTimerAsync(任务任务,Tuple< Func< T,EngineTimer< T>,Task>,T>状态,双间隔,int numberOfFires)
{
while(!this.IsCancellationRequested)
{
//仅在应有的情况下递增。
if(numberOfFires> 0)
{
this.fireCount ++;
}
等待state.Item1(state.Item2,此);
等待PerformTimerCancellationCheck(interval,numberOfFires);
}
}
私有异步任务PerformTimerCancellationCheck(double interval,int numberOfFires)
{
//如果我们已达到火灾计数,请停止。如果设置为0,则触发直到手动停止。
if(numberOfFires> 0&& this.fireCount> = numberOfFires)
{
this.Stop();
}
等待Task.Delay(TimeSpan.FromMilliseconds(interval),this.Token).ConfigureAwait(false);
}
}
然后我为该类创建了一系列单元测试。
[TestClass]
公共类EngineTimerTests
{
[TestMethod]
[TestCategory( MudDesigner)]
[TestCategory( Engine)]
[TestCategory( Engine Core)]
[Owner( Johnathon Sullinger)]
[ExpectedException(typeof(ArgumentNullException))]
public void Exception_thrown_with_null_ctor_argument()
{
//操作
new EngineTimer< ComponentFixture>(null);
}
[TestMethod]
[TestCategory( MudDesigner)]
[TestCategory( Engine)]
[TestCategory( Engine Core )]
[Owner( Johnathon Sullinger)]
public void Ctor_sets_state_property()
{
//安排
var夹具=新ComponentFixture();
//操作
var engineTimer = new EngineTimer< ComponentFixture>(夹具);
//声明
Assert.IsNotNull(engineTimer.StateData,未从构造函数分配状态。);
Assert.AreEqual(fixture,engineTimer.StateData,为计时器分配了错误的State对象。);
}
[TestMethod]
[TestCategory( MudDesigner)]
[TestCategory( Engine)]
[TestCategory( Engine Core )]
[Owner( Johnathon Sullinger)]
public void Start_sets_is_running()
{
//安排
var Fixture = new ComponentFixture();
var engineTimer = new EngineTimer< ComponentFixture>(夹具);
//操作
engineTimer.Start(0,1,0,(component,timer)=> {});
//声明
Assert.IsTrue(engineTimer.IsRunning,引擎计时器未启动。);
}
[TestMethod]
[TestCategory( MudDesigner)]
[TestCategory( Engine)]
[TestCategory( Engine Core )]
[Owner( Johnathon Sullinger)]
public void Callback_invoked_when_running()
{
//安排
var夹具=新ComponentFixture();
var engineTimer = new EngineTimer< ComponentFixture>(夹具);
bool callbackInvoked = false;
//操作
engineTimer.Start(0,1,0,(component,timer)=> {callbackInvoked = true;});
Task.Delay(20);
//声明
Assert.IsTrue(callbackInvoked,引擎计时器未按预期调用回调。);
}
}
在Visual Studio中运行单元测试覆盖率分析时2015年,它告诉我班级100%都包含单元测试。但是,我只测试了构造函数和 Start()
方法。没有任何单元测试可以触摸 Stop()
, StartAsync()
或 Dispose()
方法。
为什么Visual Studio会告诉我我处于100%的代码覆盖率?
更新
我打开了重点报道,发现未覆盖 Stop()
方法(如果我阅读了
有趣的是,该分析告诉我100%覆盖,即使覆盖范围内的高亮显示它未包含在任何单元测试路径中。
解释代码覆盖范围的方式的最简单方法如下:
- 从目标目录中获取所有dll并进行构建基于
IL代码
的有向图G
。 - 使用
G
并创建所有可能的有向路径。 - 执行测试并标记相关路径。
- 计算百分比。
具有100%代码覆盖率的方法意味着您在 UT
的执行。(基本上,该工具不知道 UT
中正在测试的是哪个类)
根据上面的描述,您可能遇到的CC行为可能来自以下至少一种选择:
- 代码覆盖工具中的错误
- 当我的CC工具处理dll的精简版时,我遇到了类似的事情。(
重建解决方案
解决了这种情况下的问题) - 至少有一个测试直接调用这些方法。
- 至少有一个测试间接调用了这些方法:通过继承,组合
等。
要理解我为您提供服务的原因,
< a href = https://i.stack.imgur.com/3fKWj.jpg rel = nofollow noreferrer>
如果您想查看特定测试类别的覆盖率,则非常简单:
- 在测试资源管理器中,右键单击->分组依据->类
- 选择要监视的类。
- 右键单击->分析选定测试的代码覆盖率。
I have a class that implements the IDisposable interface.
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);
}
}
I then created a series of unit tests for the 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.");
}
}
When I run the unit test coverage analysis in Visual Studio 2015, it tells me that the class is 100% covered by unit tests. However, I've only tested the constructor and the Start()
method. None of the unit tests touch the Stop()
, StartAsync()
or Dispose()
methods.
Why would Visual Studio tell me I am at 100% code coverage?
Update
I turned on coverage highlights and discovered that the Stop()
method is not covered (if I read this right).
It is interesting that the analysis tells me it's 100% covered, even though the coverage highlights shows that it's not included in any unit test paths.
The simplest way to explain the way code coverage works is as the following:
- Take all dlls from the target directory and build a directed graph
G
based on theIL code
. - Use
G
and creates all possible directed-paths. - Execute the tests and mark the relevant paths.
- Calculate the percentage.
Methods with 100% code coverage means you walk through all the method's paths, during your UT
's execution.(Basically the tool doesn't know which class in under test in the UT
)
Based on the above description the CC's behavior you've faced could happened from at least one of the following options:
- Bug in the code coverage tool
- I've faced something similar when my CC tool worked on deffrent version of my dlls.(
Rebuild Solution
solved the problem in this case) - At least one of your tests calls those methods directly.
- At least one of your tests calls those methods indirectly: Through inheritance, composition and etc...
To understand the reason I offer you to follow:
If you want to see the coverage from specific test class it's very easy:
- In "Test Explorer" right click -> Group By -> Class
- Select the class you want to monitor.
- Right click -> Analyze Code Coverage For Selected Tests.
这篇关于当大多数班级缺少覆盖时,代码覆盖率分析显示100%的覆盖率的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!