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

时间:2011-08-03 08:02:56

标签: c# unit-testing nunit ienumerable

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

我正在阅读并希望了解适当的测试。我正在使用。我的主要问题是我已经写了我的课并且正在使用它。它适用于我正在使用它(目前有一件事)。所以,我正在编写我的测试,只是试着想出可能出错的事情,因为我已经非正式地测试了我可能无意识地编写测试,我知道我已经检查过了。 如何在太多/细粒度测试与太少测试之间取得写入平衡?

  1. 我应该只测试公共方法/构造函数,还是应该测试每种方法?
  2. 我应该单独测试CachedStreamingEnumerable.CachedStreamingEnumerator课吗?
  3. 目前我只是在将类设置为单线程时进行测试。考虑到在检索项目并将其添加到缓存之前我可能需要等待一段时间,我如何在多线程时测试它?
  4. 我缺少哪些测试以确保良好的覆盖范围?我还有什么不需要的吗?

  5. 类的代码和下面的测试类。

    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)
                        {
    
                        }
                    }
                });
        }
    }
    

2 个答案:

答案 0 :(得分:3)

广告1)
如果你需要测试私有方法,这应该告诉你一些事情;可能你的班级有太多的责任。通常,私有方法是等待出生的独立类:-)

Ad 2)

Ad 3)
遵循与1相同的参数,如果可以避免,则可能不会在类内部完成线程功能。我记得罗伯特·马丁在“清洁代码”中读到了一些相关内容。他指出,线程是一个单独的问题,应该与业务逻辑的其他和平分开。

Ad 4)
私人方法是最难掩盖的。因此,我再次转向我的答案1.如果你的私人方法是单独的类中的公共方法,它们将更容易覆盖。此外,您的主要课程的测试将更容易理解。

此致 的Morten

答案 1 :(得分:2)

在制作测试时,我只是建议您切实可行,并遵循“少数法则”。您需要测试每个访问者或行业标准代码的每个小片段。

想想最糟糕的是什么样的事情会伤害你的班级并防范他们。检查边界条件。使用您对过去经历中可能破坏类似代码的记忆。尝试可能意外的测试数据值。

你可能不是这样做的学术练习。您可能希望确保您的类是可靠的,并且当您稍后返回以重构它时或者当您希望确保它不是其客户端类中的错误行为的原因时它将保持这种方式。

你的每一次测试都应该是有原因的,不仅仅是因为你可以在下一届TDD俱乐部会议上保持冷静!