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

标签 c# unit-testing nunit ienumerable

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

我正在阅读 并希望了解适当的测试。我正在使用 .我的主要问题是我已经编写了我的类(class)并正在使用它。它适用于我正在使用它的目的(目前的一件事)。因此,我在编写测试时只是想着可能会出错的事情,考虑到我已经非官方测试过,我可能在不知不觉中编写了我知道我已经检查过的测试。 如何在太多/细粒度测试和太少测试之间取得写入平衡?

  1. 我应该只测试公共(public)方法/构造函数还是应该测试所有方法?
  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)
                    {

                    }
                }
            });
    }
}

最佳答案

广告 1)
如果您需要测试私有(private)方法,这应该会告诉您一些信息;可能你的类(class)有太多的责任。很多时候,私有(private)方法是等待诞生的独立类:-)

广告 2)
是的

广告 3)
按照与 1 相同的论点,如果可以避免的话,线程功能可能不应该在类内部完成。我记得在 Robert Martin 的“Clean Code”中读过一些关于此的内容。他说线程是一个单独的问题,应该与业务逻辑的其他部分分开。

广告 4)
私有(private)方法是最难覆盖的。因此,我再次转向我的答案 1。如果您的私有(private)方法是单独类中的公共(public)方法,它们将更容易覆盖。另外,你的主类的测试会更容易理解。

问候, 莫腾

关于c# - 单元测试,确保良好的覆盖率,同时避免不必要的测试,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/6923361/

相关文章:

c# - 排序和 IComparable 的问题

unit-testing - 在一个单元测试下测试多个场景

javascript - 如何在 react 异步调用后测试状态更新和组件重新渲染

c# - Func NUnit 相等比较

msbuild - 以独立于平台的方式从 msbuild 项目运行 .net 可执行文件

c# - C#应用程序:从音频输出采样音频-> FFT算法->可视化

c# - 网站 Paypal 整合

c# - "GenerateBindingRedirects"任务意外失败。指定的路径、文件名或两者都太长

android - 带有 mockito 测试的简单 kotlin 类导致 MissingMethodInvocationException

.net - 如何在另一个线程上正确地对调用 UI 方法进行单元测试?