单元测试,确保良好的覆盖率,同时避免不必要的测试
本文关键字:不必要 测试 覆盖率 确保 单元测试 | 更新日期: 2023-09-27 18:04:26
我编写了class,这是一个可枚举的包装器,它缓存底层可枚举对象的结果,只有在枚举并到达缓存结果的末尾时才获取下一个元素。它可以是多线程的(在另一个线程中获取下一个项目)或单线程的(在当前线程中获取下一个项目)。
我正在阅读单元测试,并想让我的头脑在适当的测试。我用的是n单位。我的主要问题是,我已经写了我的类,并正在使用它。它适用于我正在使用它(目前一件事)。所以,我在写测试的时候,只是试着想想可能出错的地方,考虑到我已经非正式地测试过,我可能无意识地写测试,我知道我已经检查过了。我如何获得太多/细粒度测试和太少测试之间的写平衡?
- 我应该只测试公共方法/构造函数还是应该测试每个方法?
- 我应该单独测试
CachedStreamingEnumerable.CachedStreamingEnumerator
类吗? - 目前我只测试当类被设置为单线程。我如何去测试它时多线程,考虑到我可能需要等待一段时间之前,一个项目被检索并添加到缓存?
- 我错过了哪些测试来确保良好的覆盖率?有什么我已经有的不需要了吗?
类的代码,以及下面的测试类。
CachedStreamingEnumerable
/// <summary>
/// An enumerable that wraps another enumerable where getting the next item is a costly operation.
/// It keeps a cache of items, getting the next item from the underlying enumerable only if we iterate to the end of the cache.
/// </summary>
/// <typeparam name="T">The type that we're enumerating over.</typeparam>
public class CachedStreamingEnumerable<T> : IEnumerable<T>
{
/// <summary>
/// An enumerator that wraps another enumerator,
/// keeping track of whether we got to the end before disposing.
/// </summary>
public class CachedStreamingEnumerator : IEnumerator<T>
{
public class DisposedEventArgs : EventArgs
{
public bool CompletedEnumeration;
public DisposedEventArgs(bool completedEnumeration)
{
CompletedEnumeration = completedEnumeration;
}
}
private IEnumerator<T> _UnderlyingEnumerator;
private bool _FinishedEnumerating = false;
// An event for when this enumerator is disposed.
public event EventHandler<DisposedEventArgs> Disposed;
public CachedStreamingEnumerator(IEnumerator<T> UnderlyingEnumerator)
{
_UnderlyingEnumerator = UnderlyingEnumerator;
}
public T Current
{
get { return _UnderlyingEnumerator.Current; }
}
public void Dispose()
{
_UnderlyingEnumerator.Dispose();
if (Disposed != null)
Disposed(this, new DisposedEventArgs(_FinishedEnumerating));
}
object System.Collections.IEnumerator.Current
{
get { return _UnderlyingEnumerator.Current; }
}
public bool MoveNext()
{
bool MoveNextResult = _UnderlyingEnumerator.MoveNext();
if (!MoveNextResult)
{
_FinishedEnumerating = true;
}
return MoveNextResult;
}
public void Reset()
{
_FinishedEnumerating = false;
_UnderlyingEnumerator.Reset();
}
}
private bool _MultiThreaded = false;
// The slow enumerator.
private IEnumerator<T> _SourceEnumerator;
// Whether we're currently already getting the next item.
private bool _GettingNextItem = false;
// Whether we've got to the end of the source enumerator.
private bool _EndOfSourceEnumerator = false;
// The list of values we've got so far.
private List<T> _CachedValues = new List<T>();
// An object to lock against, to protect the cached value list.
private object _CachedValuesLock = new object();
// A reset event to indicate whether the cached list is safe, or whether we're currently enumerating over it.
private ManualResetEvent _CachedValuesSafe = new ManualResetEvent(true);
private int _EnumerationCount = 0;
/// <summary>
/// Creates a new instance of CachedStreamingEnumerable.
/// </summary>
/// <param name="Source">The enumerable to wrap.</param>
/// <param name="MultiThreaded">True to load items in another thread, otherwise false.</param>
public CachedStreamingEnumerable(IEnumerable<T> Source, bool MultiThreaded)
{
this._MultiThreaded = MultiThreaded;
if (Source == null)
{
throw new ArgumentNullException("Source");
}
_SourceEnumerator = Source.GetEnumerator();
}
/// <summary>
/// Handler for when the enumerator is disposed.
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
private void Enum_Disposed(object sender, CachedStreamingEnumerator.DisposedEventArgs e)
{
// The cached list is now safe (because we've finished enumerating).
lock (_CachedValuesLock)
{
// Reduce our count of (possible) nested enumerations
_EnumerationCount--;
// Pulse the monitor since this could be the last enumeration
Monitor.Pulse(_CachedValuesLock);
}
// If we've got to the end of the enumeration,
// and our underlying enumeration has more elements,
// and we're not getting the next item already
if (e.CompletedEnumeration && !_EndOfSourceEnumerator && !_GettingNextItem)
{
_GettingNextItem = true;
if (_MultiThreaded)
{
ThreadPool.QueueUserWorkItem((Arg) =>
{
AddNextItem();
});
}
else
AddNextItem();
}
}
/// <summary>
/// Adds the next item from the source enumerator to our list of cached values.
/// </summary>
private void AddNextItem()
{
if (_SourceEnumerator.MoveNext())
{
lock (_CachedValuesLock)
{
while (_EnumerationCount != 0)
{
Monitor.Wait(_CachedValuesLock);
}
_CachedValues.Add(_SourceEnumerator.Current);
}
}
else
{
_EndOfSourceEnumerator = true;
}
_GettingNextItem = false;
}
System.Collections.IEnumerator System.Collections.IEnumerable.GetEnumerator()
{
return GetEnumerator();
}
public IEnumerator<T> GetEnumerator()
{
lock (_CachedValuesLock)
{
var Enum = new CachedStreamingEnumerator(_CachedValues.GetEnumerator());
Enum.Disposed += new EventHandler<CachedStreamingEnumerator.DisposedEventArgs>(Enum_Disposed);
_EnumerationCount++;
return Enum;
}
}
}
CachedStreamingEnumerableTests
[TestFixture]
public class CachedStreamingEnumerableTests
{
public bool EnumerationsAreSame<T>(IEnumerable<T> first, IEnumerable<T> second)
{
if (first.Count() != second.Count())
return false;
return !first.Zip(second, (f, s) => !s.Equals(f)).Any(diff => diff);
}
[Test]
public void InstanciatingWithNullParameterThrowsException()
{
Assert.Throws<ArgumentNullException>(() => new CachedStreamingEnumerable<int>(null, false));
}
[Test]
public void SameSequenceAsUnderlyingEnumerationOnceCached()
{
var SourceEnumerable = Enumerable.Range(0, 10);
var CachedEnumerable = new CachedStreamingEnumerable<int>(SourceEnumerable, false);
// Enumerate the cached enumerable completely once for each item, so we ensure we cache all items
foreach (var x in SourceEnumerable)
{
foreach (var i in CachedEnumerable)
{
}
}
Assert.IsTrue(EnumerationsAreSame(Enumerable.Range(0, 10), CachedEnumerable));
}
[Test]
public void CanNestEnumerations()
{
var SourceEnumerable = Enumerable.Range(0, 10).Select(i => (decimal)i);
var CachedEnumerable = new CachedStreamingEnumerable<decimal>(SourceEnumerable, false);
Assert.DoesNotThrow(() =>
{
foreach (var d in CachedEnumerable)
{
foreach (var d2 in CachedEnumerable)
{
}
}
});
}
}
ad1)
如果你需要测试私有方法,这应该会告诉你一些事情;也许你的班级有太多的责任。通常,私有方法是等待诞生的独立类:-)
广告2)
是的
广告在与1相同的参数之后,如果可以避免的话,线程功能可能不应该在类中完成。我记得在Robert Martin的"Clean Code"一书中读到过这方面的内容。他说,线程是一个单独的问题,应该与业务逻辑的其他部分分开。
4)广告私有方法是最难涵盖的。因此,我再次回到我的答案1。如果您的私有方法是单独类中的公共方法,那么它们将更容易覆盖。而且,主类的测试也更容易理解。
问候,Morten
与其用细节来迷惑你,我只是建议你在创建测试时要实际一些,并遵循"关键少数法则"。不需要测试每个访问器或行业标准代码的每个小片段。
想想哪些事情会对你的班级造成最严重的伤害,并加以防范。检查边界条件。利用你所有的记忆,在你过去的经历中,是什么打破了类似的密码。尝试测试可能出乎意料的数据值。你可能不是作为一个学术练习来做这个。您可能想要确保您的类是可靠的,并且当您稍后回去重构它时,或者当您想要确保它不是其客户端类中错误行为的原因时,它将保持这种状态。
你的每个测试都应该是有原因的,而不仅仅是为了让你在下一次TDD俱乐部会议上表现得很酷!