单元测试,确保良好的覆盖率,同时避免不必要的测试

本文关键字:不必要 测试 覆盖率 确保 单元测试 | 更新日期: 2023-09-27 18:04:26

我编写了class,这是一个可枚举的包装器,它缓存底层可枚举对象的结果,只有在枚举并到达缓存结果的末尾时才获取下一个元素。它可以是多线程的(在另一个线程中获取下一个项目)或单线程的(在当前线程中获取下一个项目)。

我正在阅读单元测试,并想让我的头脑在适当的测试。我用的是n单位。我的主要问题是,我已经写了我的类,并正在使用它。它适用于我正在使用它(目前一件事)。所以,我在写测试的时候,只是试着想想可能出错的地方,考虑到我已经非正式地测试过,我可能无意识地写测试,我知道我已经检查过了。我如何获得太多/细粒度测试和太少测试之间的写平衡?

  1. 我应该只测试公共方法/构造函数还是应该测试每个方法?
  2. 我应该单独测试CachedStreamingEnumerable.CachedStreamingEnumerator类吗?
  3. 目前我只测试当类被设置为单线程。我如何去测试它时多线程,考虑到我可能需要等待一段时间之前,一个项目被检索并添加到缓存?
  4. 我错过了哪些测试来确保良好的覆盖率?有什么我已经有的不需要了吗?

类的代码,以及下面的测试类。

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)
是的

3)

广告在与1相同的参数之后,如果可以避免的话,线程功能可能不应该在类中完成。我记得在Robert Martin的"Clean Code"一书中读到过这方面的内容。他说,线程是一个单独的问题,应该与业务逻辑的其他部分分开。

4)

广告私有方法是最难涵盖的。因此,我再次回到我的答案1。如果您的私有方法是单独类中的公共方法,那么它们将更容易覆盖。而且,主类的测试也更容易理解。

问候,Morten

与其用细节来迷惑你,我只是建议你在创建测试时要实际一些,并遵循"关键少数法则"。不需要测试每个访问器或行业标准代码的每个小片段。

想想哪些事情会对你的班级造成最严重的伤害,并加以防范。检查边界条件。利用你所有的记忆,在你过去的经历中,是什么打破了类似的密码。尝试测试可能出乎意料的数据值。

你可能不是作为一个学术练习来做这个。您可能想要确保您的类是可靠的,并且当您稍后回去重构它时,或者当您想要确保它不是其客户端类中错误行为的原因时,它将保持这种状态。

你的每个测试都应该是有原因的,而不仅仅是为了让你在下一次TDD俱乐部会议上表现得很酷!