Saturday, April 4, 2015

Unit testing asynchronous code

Сегодня поговорим о теме, волнующей многих разработчиков, у которых возникает проблема с тестированием кода в C# при работе с многопоточностью. Многопоточность – тема сама по себе непростая, а в связке с тестированием может стать настоящим кошмаром. 
Недавно в Moq появилась возможность тестирования асинхронных методов. Мне самому интересно узнавать некоторые нюансы в ней, поэтому поделюсь с вами теми знаниями, которые смогут вам пригодиться. 
Для тестирования нашего кода мы будем использовать достаточно легковесный для тестирования фреймворк Moq. Он прост для понимания, и буквально за полчаса ознакомления с этим фреймворком вы сможете писать адекватный код для тестирования той или иной функциональности. Если вы хотите посмотреть больше примеров использования Moq, посмотрите статью Сергея Теплякова "Примеры использования Moq". Мне нравится его стиль объяснения, хотя в некоторых случаях есть немного отступлений, что, однако, не портит стиль описания, придавая некий колорит статьям. Если вы уже знаете, как работает Moq и использовали его для покрытия юнит-тестами своих проектов, тогда это будет только огромный плюс, так как вам легче будет усвоить весь этот "салат". 
Переходим к практике. Создадим новое консольное приложение и назовем его AsyncTestingSample.
Выставляйте себе по возможности сразу .NET Framework 4.5, так как в примерах будет использоваться async/await.
Как тестировать BackgroundWorker
Начнем со знакомства с подходом к тестированию в вашем проекте классов, в которых вы используете BackgroundWorker. Рассмотрим классический пример. Создадим интерфейс ICalculator, как показано ниже.
public interface ICalculator
{
    void Calculate(int number);

    event RunWorkerCompletedEventHandler CalculateCompleted;
    event Action<Exception> CalculateFailed;
}
Затем добавим реализацию Calculator.
public class Calculator : ICalculator
{
    public void Calculate(int number)
    {
        var worker = new BackgroundWorker();
        worker.DoWork += (o, args) =>
        {
            var sum = 0;
            for (int i = 0; i < number; i++)
            {
                DateTime.Now.ToString();
                sum += i;
            }

            args.Result = sum;
        };

        worker.RunWorkerCompleted += (o, args) =>
        {
            if (args.Error != null)
                CalculateFailed(args.Error);
            else
            {
                CalculateCompleted(o, args);
            }
        };

        worker.RunWorkerAsync();
    }

    public event RunWorkerCompletedEventHandler CalculateCompleted = delegate { };
    public event Action<Exception> CalculateFailed = delegate { };
}
Пример очень примитивный. У нас метод Calculate принимает целое число, а затем, когда заканчивает вычислять сумму, вызывает событие CalculateComplete, если все прошло успешно, или CalculateFailed, если произошла какая-то ошибка. Такой код тестировать очень сложно по одной простой причине: мы не знаем, сколько времени у нас будет выполняться приведенный выше код. Для тестирования этого кода нужно написать юнит-тест. Для написания теста мне понадобился вспомогательный класс, чтобы проверить с помощью Moq, что мои события вызвались.
[TestClass]
    public class CalculatorTest
    {
        [TestMethod]
        public void  Calculate_sum_for_big_number_async()
        {
            ICalculator calc = new Calculator();
            var mockCalcHelper = new Mock<CalculatorHelper>(calc);

            mockCalcHelper.Setup(x => x.Calculate(It.IsAny<int>()));
            mockCalcHelper.Setup(x => x.CalcOnCalculateFailed(It.IsAny<Exception>()));
            mockCalcHelper.Setup(x => x.CalcOnCalculateCompleted(It.IsAny<object>(), It.IsAny<RunWorkerCompletedEventArgs>()));

            calc.CalculateCompleted += mockCalcHelper.Object.CalcOnCalculateCompleted;
            calc.CalculateFailed += mockCalcHelper.Object.CalcOnCalculateFailed;

            calc.Calculate(1000000);

            Thread.Sleep(10000);

            mockCalcHelper.Verify(x => x.CalcOnCalculateCompleted(It.IsAny<object>(), It.Is<RunWorkerCompletedEventArgs>(res => (int)res.Result == 1783293664)), Times.Once);
            mockCalcHelper.Verify(x => x.CalcOnCalculateFailed(It.IsAny<Exception>()), Times.Never);
        }

        public class CalculatorHelper
        {
            private ICalculator _calc;

            public CalculatorHelper(ICalculator calc)
            {
                _calc = calc;
            }

            public virtual void CalcOnCalculateFailed(Exception exception)
            {
                
            }

            public virtual  void CalcOnCalculateCompleted(object sender, RunWorkerCompletedEventArgs runWorkerCompletedEventArgs)
            {
                
            }

            public virtual void Calculate(int number)
            {
                _calc.Calculate(number);
            }
        }
    }
Класс CalculatorHelper используется только во вспомогательных целях для покрытия тестами нашей реализации.
Результат выполнения теста показан ниже.
Вас сразу же должно насторожить использование Thread.Sleep в тесте.
calc.Calculate(1000000);

Thread.Sleep(10000);

mockCalcHelper.Verify(x => x.CalcOnCalculateCompleted(It.IsAny<object>(), It.Is<RunWorkerCompletedEventArgs>(res => (int)res.Result == 1783293664)), Times.Once);

Если у вас есть такой код, значит, архитектура построена не совсем корректно. Есть несколько способов, как это обойти. Первый способ я называю dummy testing, потому что основная суть данного подхода заключается в том, чтобы изменить асинхронное выполнение кода для тестирования в синхронное. Примитивные сценарии этот подход покрывает, и вы даже сможете проверить, что ваш код работает, но увы, он не покрывает сложные сценарии, когда у вас работает сразу несколько потоков, которые обновляют какие-то общие данные. Но к таким более сложным сценариям мы придём немного позже. Для того чтобы скрыть работу с нашим классом BackgroundWorker, нам нужно всю логику с ним спрятать за интерфейсом. Для этого создадим интерфейс IBackgroundWorkerManager, в который для начала добавим один метод.
public interface IBackgroundWorkerManager
{
    void BackgroundTask(Action<DoWorkEventArgs> action,
        Action<RunWorkerCompletedEventArgs> onCompleted,
        Action<Exception> errorAction);
}
А теперь − сама реализация класса BackgroundWorkerManager:
public class BackgroundWorkerManager : IBackgroundWorkerManager
{
    public void BackgroundTask(Action<DoWorkEventArgs> action, Action<RunWorkerCompletedEventArgs> onCompleted, Action<Exception> errorAction)
    {
        if (action == null)
            return;

        var worker = new BackgroundWorker();
        worker.DoWork += (o, args) => action(args);

        if (onCompleted != null)
            worker.RunWorkerCompleted += (o, args) =>
            {
                if (args.Error != null)
                    errorAction(args.Error);
                else
                {
                    onCompleted(args);
                }
            };

        worker.RunWorkerAsync();
    }
}
Следующим делом нам нужно поправить реализацию класса Calculator c использованием интерфейса IBackgroundWorkerManager.
public class Calculator : ICalculator
{
    private IBackgroundWorkerManager _backgroundWorkerManager;

    public Calculator(IBackgroundWorkerManager backgroundWorkerManager)
    {
        _backgroundWorkerManager = backgroundWorkerManager;
    }

    public void Calculate(int number)
    {
        _backgroundWorkerManager.BackgroundTask(doWork =>
        {
            doWork.Result = Sum(number);
        },
        onComplete => CalculateCompleted(null, onComplete),
        error => CalculateFailed(error));
    }

    private int Sum(int number)
    {
        var sum = 0;
        for (int i = 0; i < number; i++)
        {
            DateTime.Now.ToString();
            sum += i;
        }
        return sum;
    }

    public event RunWorkerCompletedEventHandler CalculateCompleted = delegate { };
    public event Action<Exception> CalculateFailed = delegate { };
}
Теперь настало самое время реализовать наш dummy класс, который сделает нашу асинхронную операцию синхронной.
public class DummyBachgroundWorkerManager : IBackgroundWorkerManager
{
    public void BackgroundTask(Action<DoWorkEventArgs> action, Action<RunWorkerCompletedEventArgs> onCompleted, Action<Exception> errorAction)
    {
        try
        {
            var eventArgs = new DoWorkEventArgs(null);
            action(eventArgs);

            onCompleted(new RunWorkerCompletedEventArgs(eventArgs.Result, nullfalse));
        }
        catch (Exception ex)
        {
            errorAction(ex);
        }
    }
}
Вот как протестировать наш предыдущий вариант без использования Thead.Sleep:
[TestMethod]
public void  Calculate_sum_for_big_number_async()
{
    IBackgroundWorkerManager backgroundWorker = new DummyBachgroundWorkerManager();
    ICalculator calc = new Calculator(backgroundWorker);
    var mockCalcHelper = new Mock<CalculatorHelper>(calc);

    mockCalcHelper.Setup(x => x.Calculate(It.IsAny<int>()));
    mockCalcHelper.Setup(x => x.CalcOnCalculateFailed(It.IsAny<Exception>()));
    mockCalcHelper.Setup(x => x.CalcOnCalculateCompleted(It.IsAny<object>(), It.IsAny<RunWorkerCompletedEventArgs>()));

    calc.CalculateCompleted += mockCalcHelper.Object.CalcOnCalculateCompleted;
    calc.CalculateFailed += mockCalcHelper.Object.CalcOnCalculateFailed;

    calc.Calculate(1000000);

    mockCalcHelper.Verify(x => x.CalcOnCalculateCompleted(It.IsAny<object>(), It.Is<RunWorkerCompletedEventArgs>(res => (int)res.Result == 1783293664)), Times.Once);
    mockCalcHelper.Verify(x => x.CalcOnCalculateFailed(It.IsAny<Exception>()), Times.Never);
}
Как видите, метод стал намного читабельнее, и теперь его намного проще тестировать. Как минимум мы убрали паузу из-за Sleep, а также избежали различных "мистических" мест ожидания.
Пришло время третьего варианта, который работает с помощью синхронизации потоков. От предыдущего варианта, который асинхронный метод делает синхронным, в третьем варианте вы просто добавляете управление вашей многопоточностью. Для этого перепишем наш класс DummyBackgroundWorkerManager с использованием класса AutoResetEvent.
public class DummyBachgroundWorkerManager : IBackgroundWorkerManager
{
    private readonly AutoResetEvent _autoResetEvent = new AutoResetEvent(false);
    public void BackgroundTask(Action<DoWorkEventArgs> action, Action<RunWorkerCompletedEventArgs> onCompleted, Action<Exception> errorAction)
    {
        var worker = new BackgroundWorker();
        worker.DoWork += (o, args) => action(args);

        worker.RunWorkerCompleted += (o, args) =>
        {
            if (args.Error != null)
                errorAction(args.Error);
            else
            {
                onCompleted(args);
            }
            _autoResetEvent.Set();
        };

        worker.RunWorkerAsync();
        _autoResetEvent.WaitOne();
    }
}
Все остальное останется таким же, как и в предыдущем варианте. Как по мне, вышло очень неплохо. Но нужно помнить об одной коварной особенности данного подхода. Ваш метод RunWorkerComplete, на который вы подписались, должен быть вызван гарантировано, иначе поведение вашего кода будет не таким, как вы его ожидаете. Ваш код просто повиснет и будет ожидать, когда же вы вызовите для вашей переменной _autoResetEvent метод Set.
Давайте перечислим все рассмотренные способы:
  1. Использование Thread.Sleep (чего лучше не делать).
  2. Использовать интерфейс, который будет скрывать логику работы с асинхронными операциями, и написать реализацию, которая также будет работать для тестов в синхронном режиме.
  3. Использовать объекты синхронизации наподобие AutoResetEvent с таким же сокрытием асинхронных операций за интерфейсом.
Как тестировать Task
Тестирование тасков имеет свои отличия, по сравнению с тестированием асинхронного кода, который работает с BackgroundWorker. В MSDN Magazine этой цели посвящена целая статья ("Async Programming"). Мы также с вами рассмотрим, как тестировать таски с использованием ключевых слов async/await. Но поскольку мы понимаем, что не все используют 4.5 фреймворк и многие разработчики до сих пор сидят на .NET Framework 4.0 (в компании, где я работаю, только недавно проект перевели с огромным скрипом на .NET 4.5), поэтому начнем с подхода, который будет актуален для четвертой версии фреймворка. Самый простой пример с использованием Thread.Sleep мы сейчас и рассмотрим. Код не стоит так писать, но для того чтобы понять, как это работает, думаю, можно ознакомиться.
Изменения интерфейса ICalculator:
public interface ICalculator
{
    void Calculate(int number);

    event Action<int> CalculateCompleted;
    event Action<Exception> CalculateFailed;
}
Изменения класса Calculator:
public class Calculator : ICalculator
{
    public void Calculate(int number)
    {
        var task = Task.Factory.StartNew(() => Sum(number));

        task.ContinueWith(res =>
        {
            if (res.IsFaulted)
            {
                CalculateFailed(res.Exception);
            }
            else
            {
                CalculateCompleted(res.Result);
            }
        });
    }

    private int Sum(int number)
    {
        var sum = 0;
        for (int i = 0; i < number; i++)
        {
            DateTime.Now.ToString();
            sum += i;
        }
        return sum;
    }

    public event Action<int> CalculateCompleted = delegate { };
    public event Action<Exception> CalculateFailed = delegate { };
}
Также изменения немного затронули наш вспомогательный класс для тестирования CalculatorHelper.
public class CalculatorHelper
{
    private ICalculator _calc;

    public CalculatorHelper(ICalculator calc)
    {
        _calc = calc;
    }

    public virtual void CalcOnCalculateFailed(Exception exception)
    {
                
    }

    public virtual  void CalcOnCalculateCompleted(int res)
    {
                
    }

    public virtual void Calculate(int number)
    {
        _calc.Calculate(number);
    }
}
Изменения самого теста:
[TestMethod]
public void Calculate_sum_for_big_number_with_task()
{
    ICalculator calc = new Calculator();
    var mockCalcHelper = new Mock<CalculatorHelper>(calc);

    mockCalcHelper.Setup(x => x.Calculate(It.IsAny<int>()));
    mockCalcHelper.Setup(x => x.CalcOnCalculateFailed(It.IsAny<Exception>()));
    mockCalcHelper.Setup(x => x.CalcOnCalculateCompleted(It.IsAny<int>()));

    calc.CalculateCompleted += mockCalcHelper.Object.CalcOnCalculateCompleted;
    calc.CalculateFailed += mockCalcHelper.Object.CalcOnCalculateFailed;

    calc.Calculate(1000000);

    Thread.Sleep(10000);
    mockCalcHelper.Verify(x => x.CalcOnCalculateCompleted(It.Is<int>(res => res == 1783293664)), Times.Once);
    mockCalcHelper.Verify(x => x.CalcOnCalculateFailed(It.IsAny<Exception>()), Times.Never);
}
Для того чтобы показать изменения, я просто создал другую функцию. Успешное прохождения теста вы можете увидеть на экране ниже.
Второй способ работает по аналогичному принципу, как и способ, рассмотренный для BackgroundWorker вначале главы, с тем отличием, что, как мы помним, класс Task<TResult> у нас Generic, и мы можем слегка расширить наш калькулятор. 
Создадим отдельный интерфейс IAsyncManager, в который добавим один метод BackgroundTask, как мы это делали раньше, по аналогии с интерфейсом IBackgroundWorkerManager.
public interface IAsyncManager
{
    void BackgroundTask<T>(Func<T> action,
                            Action<T> onCompleted,
                            Action<Exception> logErrorAction);
}
Реализация данного интерфейса приведена в классе AsyncManager.
public class AsyncManager : IAsyncManager
{
    public void BackgroundTask<T>(Func<T> action,
        Action<T> onCompleted,
        Action<Exception> logErrorAction)
    {
        var task = Task.Factory.StartNew(action);

        task.ContinueWith(x =>
        {
            if (x.IsFaulted)
                logErrorAction(x.Exception);
            else
            {
                onCompleted(x.Result);
            }
        });
            
    }
}
И более элегантная для версии 4.5 переписанная функция BackgroundTask с использованием async/await:
public class AsyncManager : IAsyncManager
{
    public async void BackgroundTask<T>(Func<T> action,
        Action<T> onCompleted,
        Action<Exception> logErrorAction)
    {
        var task = Task.Factory.StartNew(action);
        try
        {
            var result = await task;
            onCompleted(result);
        }
        catch (Exception ex)
        {
            logErrorAction(ex);
        }
    }
}
Интерфейс ICalculator и класс Calculator мы сделаем также generic версии. Поэтому предлагаю реализовать новые интерфейсы, чтобы не ломать старую логику. Назовем их, соответственно, IGeneralCalculator и GeneralCalculator. Ну и слегка поменяем его логику, чтобы не передавать ему параметр, а передавать сразу метод, который что-то очень долго считает и возвращает результат.
Интерфейс IGeneralCalculator:
public interface IGeneralCalculator<T>
{
    void Calculate(Func<T> action);

    event Action<T> CalculateCompleted;
    event Action<Exception> CalculateFailed;
}
Реализация интерфейса IGeneralCalculator в классе GeneralCalculator:
public class GeneralCalculator<T> : IGeneralCalculator<T>
{
    private readonly IAsyncManager _asyncManager;
    public GeneralCalculator(IAsyncManager asyncManager)
    {
        _asyncManager = asyncManager;
    }

    public void Calculate(Func<T> action)
    {
        _asyncManager.BackgroundTask(action,
            onCompleted => CalculateCompleted(onCompleted),
            error => CalculateFailed(error));
    }

    public event Action<T> CalculateCompleted = delegate {};
    public event Action<Exception> CalculateFailed = delegate { };
}
Наш класс CalculatorHelper тоже слегка изменился.
public class CalculatorHelper<T>
{
    private IGeneralCalculator<T> _calc;

    public CalculatorHelper(IGeneralCalculator<T> calc)
    {
        _calc = calc;
    }

    public virtual void CalcOnCalculateFailed(Exception exception)
    {

    }

    public virtual void CalcOnCalculateCompleted(T res)
    {

    }

    public virtual void Calculate(Func<T> func)
    {
        _calc.Calculate(func);
    }
}
Теперь у нас есть два пути реализации ожидания выполнения таска: сделать dummy класс по аналогии к примеру по BackgroundWorker, либо дженерик-методом, который сделает подобное. Я сделаю реализацию в виде дженерик-метода, поскольку если вы до этого не стыкались с использованием этого метода в Moq, то вам может быть интересен такой подход.
private void MockObject<T>(Mock<IAsyncManager> baseMock)
{
    baseMock.Setup(pa => pa.BackgroundTask(It.IsAny<Func<T>>(), It.IsAny<Action<T>>(), It.IsAny<Action<Exception>>()))
        .Callback<Func<T>, Action<T>, Action<Exception>>((action, onCompleted, logErrorAction) =>
        {
            var task = Task<T>.Factory.StartNew(action);
                    
            try
            {
                Task.WaitAll(new[] { task });
                var result = task;
                onCompleted(result.Result);
            }
            catch (Exception ex)
            {
                logErrorAction(ex);
            }
        });
}
Ниже приведена полная реализация теста.
[TestMethod]
public void Calculate_letter_number_with_task()
{
    var asyncManagerMock = new Mock<IAsyncManager>();
    MockObject<string>(asyncManagerMock);
    var calculator = new GeneralCalculator<string>(asyncManagerMock.Object);

    var mockCalcHelper = new Mock<CalculatorHelper<string>>(calculator);

    mockCalcHelper.Setup(x => x.Calculate(It.IsAny<Func<string>>()));
    mockCalcHelper.Setup(x => x.CalcOnCalculateFailed(It.IsAny<Exception>()));
    mockCalcHelper.Setup(x => x.CalcOnCalculateCompleted(It.IsAny<string>()));

    calculator.CalculateCompleted += mockCalcHelper.Object.CalcOnCalculateCompleted;
    calculator.CalculateFailed += mockCalcHelper.Object.CalcOnCalculateFailed;

    calculator.Calculate(GetData);

    mockCalcHelper.Verify(x => x.CalcOnCalculateCompleted(It.Is<string>(res => res != null)), Times.Once);
    mockCalcHelper.Verify(x => x.CalcOnCalculateFailed(It.IsAny<Exception>()), Times.Never);
}

private string GetData()
{
    string result = string.Empty;
    for (int i = 0; i < 1000; i++)
    {
        DateTime.Now.ToString();
        result += i.ToString();
    }
    return result;
}

private void MockObject<T>(Mock<IAsyncManager> baseMock)
{
    baseMock.Setup(pa => pa.BackgroundTask(It.IsAny<Func<T>>(), It.IsAny<Action<T>>(), It.IsAny<Action<Exception>>()))
        .Callback<Func<T>, Action<T>, Action<Exception>>((action, onCompleted, logErrorAction) =>
        {
            var task = Task<T>.Factory.StartNew(action);
                    
            try
            {
                Task.WaitAll(new[] { task });
                var result = task;
                onCompleted(result.Result);
            }
            catch (Exception ex)
            {
                logErrorAction(ex);
            }
        });
}
Результат выполнения можно увидеть на рисунке ниже.
Теперь немного нарушим наш подход и рассмотрим, как можно тестировать асинхронные вызовы с использованием async/await.
[TestMethod]
public async Task Test_culculation_big_number_with_AsyncAwait()
{
    var task = Task.Run(() =>
    {
        var sum = 0;
        for (int i = 0; i < 1000000; i++)
        {
            DateTime.Now.ToString();
            sum += i;
        }

        return sum;
    });

    var result = await task;

    Assert.AreEqual(result, 1783293664);
}

Результат показан ниже.
Это простой способ, если у вас таски доступны сразу. А теперь сделаем то же самое, только добавим в наш интерфейс, который мы создавали ранее, IAsynManager метод, который будет возвращать Task.
public interface IAsyncManager
{
    void BackgroundTask<T>(Func<T> action,
                            Action<T> onCompleted,
                            Action<Exception> logErrorAction);

    Task<T> BackgroundTaskAsync<T>(Func<T> action);
}
Слегка изменится сама реализация AsyncManager.
public class AsyncManager : IAsyncManager
{
    public async void BackgroundTask<T>(Func<T> action,
        Action<T> onCompleted,
        Action<Exception> logErrorAction)
    {
        var task = Task.Factory.StartNew(action);
        try
        {
            var result = await task;
            onCompleted(result);
        }
        catch (Exception ex)
        {
            logErrorAction(ex);
        }
    }

    public Task<T> BackgroundTaskAsync<T>(Func<T> action)
    {
        return Task.Factory.StartNew(action);
    }
}
Используем приведенный код по аналогии к примеру с async/await, рассмотренному выше.
Ну и напоследок еще один пример, в котором мы добавили еще один метод в наш класс GeneralCalculator:
public Task<int> Sum()
{
    return _asyncManager.BackgroundTaskAsync(() => 2 + 3);
}
Соответственно, этот же метод объявили в интерфейсе IGeneralCalculator. Теперь посмотрим на то, как это использовать. Перепишем наш созданный ранее stub следующим образом:
public class CalculatorHelper<T>
{
    private IGeneralCalculator<T> _calc;

    public CalculatorHelper(IGeneralCalculator<T> calc)
    {
        _calc = calc;
    }

    public Task<int> Sum()
    {
        return _calc.Sum();
    }
}
Наконец, использование всего этого в юнит-тесте:
[TestMethod]
public async Task Test_async_task_methods()
{
    var mock = new Mock<IGeneralCalculator<int>>();
    mock.Setup(x => x.Sum()).Returns(() => Task.FromResult(58));
    //mock.Setup(x => x.Sum()).ReturnsAsync(58);
    var calculator = new CalculatorHelper<int>(mock.Object);

    var sum = await calculator.Sum();

    Assert.AreEqual(sum, 58);
}
Результат успешного прохождения теста можно увидеть на картинке ниже.
Примечание 1. Избегайте использования async void в своих приложениях. Во-первых, это нарушает подход Best Practices in Asynchronous Programming. Во-вторых, такие тесты вы не сможете нормально и полноценно покрыть тестами. Разве что подход, аналогичный использованию враппера, который асинхронную логику работы будет делать синхронной. Вы можете посмотреть статью "Avoid async void methods", чтобы увидеть пример написания такого кода, который позволит проверить ваши сборки на использование async void методов.
Я вряд ли смогу рассказать более интересно тему асинхронного тестирования с помощью async/await, чем это сделано в статьях, указанных выше в примечании. Да и благо, Moq фреймворк наконец-то нормально позволяет тестировать такой код. Но надеюсь, что приведенные мной примеры, которые позволяют тестировать таски, начиная с .NET Framework 4.0, возможно, вам пригодятся.
Примечание 2. Ни в коем случае не используйте объекты синхронизации внутри тасков. Если у вас внутри таска используется, например, мютекси, локи или ManualResetEvent/AutoResetEvent или другие, то примерно в 90% случаев это означает, что вы написали неверный код. Подумайте над тем, чтобы его трансформировать, если это возможно. Благо, таски очень продуманные и позволяют это сделать в большинстве случаев.

Итоги
Сегодня мы рассмотрели, как можно протестировать асинхронный код с помощью юнит-тестов. Для примера мы рассмотрели использование класса BackgroundWorker и Task. Подходы, которые описаны при тестировании BackgroundWorker, также применимы, если вы используете явно класс Thread в своих приложениях. Но такие приложения очень и очень сложно тестировать, поэтому основной совет, который хотелось бы дать, – это избегать жёсткой связанности кода. Чем меньше связность кода, тем легче его покрывать тестами. Второй совет: если вы хотите тестировать ваш код, который использует BackgroundWorkerThreadDispatcher и другие классы, позволяющие работать с потоками, старайтесь скрывать эти классы за интерфейсами, такими как IBackgroundWorkerManager и IAsyncManager. Это позволит как минимум удобно мокать ваш код с помощью того же Moq, что даст возможность тестировать не только реализацию вашего метода, но и поведение вашего кода, что бывает немаловажно при разработке. Надеюсь, что статья получилась не очень скучной, и вы сможете применить полученные здесь знания на практике. 

No comments:

Post a Comment