Модульное тестирование асинхронной функции - PullRequest
21 голосов
/ 22 января 2010

В следующем примере кода у меня есть класс Async Calculator. Это вводится с помощью ICalc, который будет синхронным калькулятором. Я использую инъекцию зависимостей и высмеиваю ICalc, потому что это напоминает мой истинный сценарий, хотя я предполагаю, что насмешка на самом деле не имеет отношения к вопросу. AsyncCalc имеет функцию, которая будет вызывать другую функцию асинхронно - принимая обратный вызов в качестве параметра. И когда вызов асинхронной функции завершится, обратный вызов будет вызван результатом.

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

Теперь у меня вопрос: нахожусь ли я на правильном пути, тестирую асинхронную функцию или кто-нибудь может помочь мне встать на правильный путь? Что было бы лучше, если бы я мог гарантировать, что обратный вызов запускается сразу - и желательно в том же потоке, я думаю? Можно / нужно ли это сделать?

public interface ICalc
{
    int AddNumbers(int a, int b);
}

public class AsyncCalc
{
    private readonly ICalc _calc;
    public delegate void ResultProcessor(int result);
    public delegate int AddNumbersAsyncCaller(int a, int b);

    public AsyncCalc(ICalc calc)
    {
        _calc = calc; 
    }

    public void AddNumbers(int a, int b, ResultProcessor resultProcessor)
    {
        var caller = new AddNumbersAsyncCaller(_calc.AddNumbers);
        caller.BeginInvoke(a, b, new AsyncCallback(AddNumbersCallbackMethod), resultProcessor);
    }

    public void AddNumbersCallbackMethod(IAsyncResult ar)
    {
        var result = (AsyncResult)ar;
        var caller = (AddNumbersAsyncCaller)result.AsyncDelegate;
        var resultFromAdd = caller.EndInvoke(ar);

        var resultProcessor = ar.AsyncState as ResultProcessor;
        if (resultProcessor == null) return;

        resultProcessor(resultFromAdd);
    }             
}

[Test]
public void TestingAsyncCalc()
{
    var mocks = new MockRepository();
    var fakeCalc = mocks.DynamicMock<ICalc>();

    using (mocks.Record())
    {
        fakeCalc.AddNumbers(1, 2);
        LastCall.Return(3);
    }

    var asyncCalc = new AsyncCalc(fakeCalc);
    asyncCalc.AddNumbers(1, 2, TestResultProcessor);
}

public void TestResultProcessor(int result)
{
    Assert.AreEqual(3, result);
}

Ответы [ 2 ]

23 голосов
/ 22 января 2010

Вы можете использовать ManualResetEvent для синхронизации своих потоков.

В следующем примере тестовый поток заблокирует вызов completion.WaitOne(). Обратный вызов для асинхронного вычисления сохраняет результат, и , а затем сигнализирует событие, вызывая completion.Set().

[Test]
public void TestingAsyncCalc()
{
    var mocks = new MockRepository();
    var fakeCalc = mocks.DynamicMock<ICalc>();

    using (mocks.Record())
    {
        fakeCalc.AddNumbers(1, 2);
        LastCall.Return(3);
    }

    var asyncCalc = new AsyncCalc(fakeCalc);

    var completion = new ManualResetEvent(false);
    int result = 0;
    asyncCalc.AddNumbers(1, 2, r => { result = r; completion.Set(); });
    completion.WaitOne();

    Assert.AreEqual(3, calcResult);
}

// ** USING AN ANONYMOUS METHOD INSTEAD
// public void TestResultProcessor(int result)
// {
//     Assert.AreEqual(3, result);
// }
0 голосов
/ 15 августа 2013

Вы также можете использовать класс «test runner» для запуска утверждений в цикле. Цикл будет запускать утверждения в try / catch. Обработчик исключений просто попытается запустить утверждения еще раз, пока не истечет время ожидания. Я недавно написал в блоге об этой технике. Пример написан на отличном языке, но применим к любому языку. Вместо передачи Закрытия, вы должны передать Действие в c #.

http://www.greenmoonsoftware.com/2013/08/asynchronous-functional-testing/

...