Модульное тестирование, обеспечивающее хорошее покрытие, избегая ненужных тестов - PullRequest
4 голосов
/ 03 августа 2011

Я написал класс, который является перечислимой оболочкой, которая кэширует результаты базового перечислимого, получая только следующий элемент, если мы перечислим и достигнем конца кэшированных результатов. Он может быть многопоточным (получение следующего элемента в другом потоке) или однопоточным (получение следующего элемента в текущем потоке).

Я читаю и хотел бы обдумать соответствующие тесты. Я использую . Моя главная проблема в том, что я уже написал свой класс и использую его. Это работает для того, для чего я его использую (одна вещь в настоящее время). Итак, я пишу свои тесты, просто пытаясь думать о вещах, которые могут пойти не так, учитывая то, что я тестировал неофициально Я, вероятно, бессознательно пишу тесты, которые я знаю, я уже проверял. Как я могу получить баланс записи между слишком многими / детальными тестами и слишком малым количеством тестов?

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

                    }
                }
            });
    }
}

Ответы [ 2 ]

3 голосов
/ 03 августа 2011

Объявление 1)
Если вам нужно протестировать приватные методы, это должно вам кое-что сказать; возможно, у вашего класса слишком много обязанностей. Довольно часто частные методы - это отдельные классы, ожидающие своего рождения :-)

Объявление 2)
Да

Объявление 3)
Следуя тому же аргументу, что и 1, функциональность многопоточности, вероятно, не должна выполняться внутри класса, если ее можно избежать. Я вспоминаю, что читал об этом в «Чистом коде» Роберта Мартина. Он заявляет, что что-то в этом роде является отдельной задачей, которая должна быть отделена от других аспектов бизнес-логики.

Объявление 4)
Частные методы труднее всего охватить. Таким образом, я снова перехожу к своему ответу 1. Если бы ваши частные методы были открытыми методами в отдельных классах, их было бы гораздо проще охватить. Кроме того, тест вашего основного класса будет легче понять.

С уважением, Morten

2 голосов
/ 04 августа 2011

Вместо того, чтобы озадачивать вас деталями, я бы просто посоветовал вам быть практичным и следовать «закону критического мало» при создании ваших тестов.Вы не должны тестировать каждый метод доступа или каждый небольшой фрагмент стандартного отраслевого кода.

Подумайте, какие вещи повредят вашему классу хуже всего, и защититесь от них.Проверьте граничные условия.Воспользуйтесь любыми воспоминаниями о том, что могло сломать подобный код в вашем прошлом опыте.Попробуйте проверить значения данных, которые могут быть неожиданными.

Вы, вероятно, не делаете это как учебное упражнение.Вы, вероятно, хотите убедиться, что ваш класс надежен и что он останется таким, когда вы вернетесь к нему позже, чтобы реорганизовать его или когда вы хотите убедиться, что он не является причиной неправильного поведения в одном из его клиентских классов.

Каждый твой тест должен быть там по причине, а не только для того, чтобы ты мог быть крутым на следующей встрече клуба TDD!

...