Модульное тестирование с Mocks, когда SUT использует Task Parallel Libaray - PullRequest
9 голосов
/ 29 апреля 2010

Я пытаюсь выполнить модульное тестирование / проверить, что метод вызывается по зависимости тестируемой системы (SUT).

  • Зависимость - IFoo.
  • Зависимый класс - IBar.
  • IBar реализован как Bar.
  • Bar вызовет Start () для IFoo в новой задаче (System.Threading.Tasks.), Когда Start () вызывается для экземпляра Bar.

Юнит-тест (Moq):

    [Test]
    public void StartBar_ShouldCallStartOnAllFoo_WhenFoosExist()
    {
        //ARRANGE

        //Create a foo, and setup expectation
        var mockFoo0 = new Mock<IFoo>();
        mockFoo0.Setup(foo => foo.Start());

        var mockFoo1 = new Mock<IFoo>();
        mockFoo1.Setup(foo => foo.Start());


        //Add mockobjects to a collection
        var foos = new List<IFoo>
                       {
                           mockFoo0.Object,
                           mockFoo1.Object
                       };

        IBar sutBar = new Bar(foos);

        //ACT
        sutBar.Start(); //Should call mockFoo.Start()

        //ASSERT
        mockFoo0.VerifyAll();
        mockFoo1.VerifyAll();
    }

Реализация IBar as Bar:

    class Bar : IBar
    {
        private IEnumerable<IFoo> Foos { get; set; }

        public Bar(IEnumerable<IFoo> foos)
        {
            Foos = foos;
        }

        public void Start()
        {
            foreach(var foo in Foos)
            {
                Task.Factory.StartNew(
                    () =>
                        {
                            foo.Start();
                        });
            }
        }
    }

Moq Исключение:

*Moq.MockVerificationException : The following setups were not matched:
IFoo foo => foo.Start() (StartBar_ShouldCallStartOnAllFoo_WhenFoosExist() in
FooBarTests.cs: line 19)*

Ответы [ 3 ]

7 голосов
/ 01 декабря 2010

@ dpurrington & @StevenH: Если мы начнем помещать подобные вещи в наш код

sut.Start();
Thread.Sleep(TimeSpan.FromSeconds(1)); 

и у нас есть тысячи «модульных» тестов, после чего наши тесты начинают выполняться в минутах, а не в секундах. Если у вас было, например, 1000 модульных тестов, вам будет сложно запустить тесты менее чем за 5 секунд, если кто-то пошел и засорил базу тестового кода с помощью Thread.Sleep.

Я полагаю, что это плохая практика, если только мы явно не проводим интеграционное тестирование.

Я бы предложил использовать интерфейс System.Concurrency.IScheduler из System.CoreEx.dll и внедрить реализацию TaskPoolScheduler.

Это мое предложение о том, как это должно быть реализовано

using System.Collections.Generic;
using System.Concurrency;
using Moq;
using NUnit.Framework;

namespace StackOverflowScratchPad
{
    public interface IBar
    {
        void Start(IEnumerable<IFoo> foos);
    }

    public interface IFoo
    {
        void Start();
    }

    public class Bar : IBar
    {
        private readonly IScheduler _scheduler;

        public Bar(IScheduler scheduler)
        {
            _scheduler = scheduler;
        }

        public void Start(IEnumerable<IFoo> foos)
        {
            foreach (var foo in foos)
            {
                var foo1 = foo;  //Save to local copy, as to not access modified closure.
                _scheduler.Schedule(foo1.Start);
            }
        }
    }

    [TestFixture]
    public class MyTestClass
    {
        [Test]
        public void StartBar_ShouldCallStartOnAllFoo_WhenFoosExist()
        {
            //ARRANGE
            TestScheduler scheduler = new TestScheduler();
            IBar sutBar = new Bar(scheduler);

            //Create a foo, and setup expectation
            var mockFoo0 = new Mock<IFoo>();
            mockFoo0.Setup(foo => foo.Start());

            var mockFoo1 = new Mock<IFoo>();
            mockFoo1.Setup(foo => foo.Start());

            //Add mockobjects to a collection
            var foos = new List<IFoo>
                       {
                           mockFoo0.Object,
                           mockFoo1.Object
                       };

            //ACT
            sutBar.Start(foos); //Should call mockFoo.Start()
            scheduler.Run();

            //ASSERT
            mockFoo0.VerifyAll();
            mockFoo1.VerifyAll();
        }
    }
}

Теперь это позволяет тесту работать на полной скорости без использования Thread.Sleep.

Обратите внимание, что контракты были изменены для принятия IScheduler в конструкторе Bar (для внедрения зависимостей), и теперь IEnumerable передается методу IBar.Start. Я надеюсь, что это имеет смысл, почему я сделал эти изменения.

Скорость тестирования является первым и наиболее очевидным преимуществом этого. Второе и, возможно, более важное преимущество этого процесса заключается в том, что вы привносите в свой код более сложный параллелизм, что делает тестирование чрезвычайно сложным. Интерфейс IScheduler и TestScheduler позволяют запускать детерминированные «модульные тесты» даже в условиях более сложного параллелизма.

0 голосов
/ 01 февраля 2012

Thread.Sleep () - определенно плохая идея. Я несколько раз читал на SO: «Настоящие приложения не спят». Примите это как хотите, но я согласен с этим утверждением. Особенно во время юнит-тестов. Если ваш тестовый код создает ложные сбои, ваши тесты хрупкие.

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

Моя реализация включает в себя изменение тестируемого класса и тестируемого метода.

class Bar : IBar
{
    private IEnumerable<IFoo> Foos { get; set; }
    internal CountdownEvent FooCountdown;

    public Bar(IEnumerable<IFoo> foos)
    {
        Foos = foos;
    }

    public void Start()
    {
        FooCountdown = new CountdownEvent(foo.Count);

        foreach(var foo in Foos)
        {
            Task.Factory.StartNew(() =>
            {
                foo.Start();

                // once a worker method completes, we signal the countdown
                FooCountdown.Signal();
            });
        }
    }
}

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

Причина, по которой внутренний модификатор доступа используется для CountdownEvent, заключается в том, что я обычно устанавливаю свойства и методы как внутренние, когда модульным тестам необходим доступ к ним. Затем я добавляю новый атрибут сборки в проверяемую сборку Properties\AssemblyInfo.cs, чтобы внутренние компоненты были доступны для тестового проекта.

[assembly: InternalsVisibleTo("FooNamespace.UnitTests")]

В этом примере FooCountdown будет ждать 3 сигнала, если в Foos есть 3 объекта foo.

Теперь вы ждете, пока FooCountdown завершит обработку сигнала, чтобы вы могли продолжить свою жизнь и прекратить тратить впустую циклы процессора на Thread.Sleep ().

[Test]
public void StartBar_ShouldCallStartOnAllFoo_WhenFoosExist()
{
    //ARRANGE

    var mockFoo0 = new Mock<IFoo>();
    mockFoo0.Setup(foo => foo.Start());

    var mockFoo1 = new Mock<IFoo>();
    mockFoo1.Setup(foo => foo.Start());


    //Add mockobjects to a collection
    var foos = new List<IFoo> { mockFoo0.Object, mockFoo1.Object };

    IBar sutBar = new Bar(foos);

    //ACT
    sutBar.Start(); //Should call mockFoo.Start()
    sutBar.FooCountdown.Wait(); // this blocks until all parallel tasks in sutBar complete

    //ASSERT
    mockFoo0.VerifyAll();
    mockFoo1.VerifyAll();
}
0 голосов
/ 29 апреля 2010

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

...