Как выполнить модульное тестирование метода, содержащего асинхронный вызов? - PullRequest
4 голосов
/ 18 февраля 2009

У меня есть метод, который содержит асинхронный вызов, как это:

public void MyMethod() {
    ...
    (new Action<string>(worker.DoWork)).BeginInvoke(myString, null, null);
    ...
}

Я использую Unity и создание фиктивных объектов не проблема, но как я могу проверить, что DoWork вызывается, не беспокоясь о состоянии гонки?

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


РЕДАКТИРОВАТЬ: Хорошо, я думал, что я мог бы задать этот вопрос в очень общей форме, но, похоже, мне нужно более подробно остановиться на этой проблеме:

Я хочу создать тест для вышеупомянутого MyMethod, поэтому я делаю что-то вроде этого:

[TestMethod]
public void TestMyMethod() {
   ...setup...
   MockWorker worker = new MockWorker();
   MyObject myObj = new MyObject(worker);
   ...assert preconditions...
   myObj.MyMethod();
   ...assert postconditions...
}

Наивным подходом было бы создать MockWorker (), который просто устанавливает флаг при вызове DoWork, и проверять этот флаг в постусловиях. Это, конечно, привело бы к состоянию гонки, когда постусловия проверяются перед установкой флага в MockWorker.

Более правильный подход (который я, вероятно, в конечном итоге буду использовать) заключается в использовании дескриптора ожидания:

class MockWorker : Worker {
    public AutoResetEvent AutoResetEvent = new AutoResetEvent();

    public override void DoWork(string arg) {
        AutoResetEvent.Set();
    }
}

... и используйте следующее утверждение:

Assert.IsTrue(worker.AutoResetEvent.WaitOne(1000, false));

Это использует семафороподобный подход, что хорошо ... но в теории может произойти следующее:

  1. BeginInvoke вызывается у моего делегата DoWork
  2. По какой-то причине ни основной поток, ни поток DoWork не получают время выполнения в течение 1000 мс.
  3. Основному потоку дается время выполнения, и из-за тайм-аута утверждение не выполняется, хотя поток DoWork еще не выполнен.

Я неправильно понял, как работает AutoResetEvent? Я просто слишком параноик, или есть разумное решение этой проблемы?

Ответы [ 3 ]

3 голосов
/ 18 февраля 2009

При тестировании прямого делегата просто используйте вызов EndInvoke, чтобы убедиться, что делегат вызван и возвращает соответствующее значение. Например.

var del = new Action<string>(worker.DoWork);
var async = del.BeginInvoke(myString,null,null);
...
var result = del.EndInvoke(async);

EDIT

ОП прокомментировал, что они пытаются провести модульное тестирование MyMethod по сравнению с методом worker.DoWork.

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

EDIT2

[OP] По какой-то причине ни основному потоку, ни потоку DoWork не дается время выполнения в течение 1000 мс.

Этого не произойдет. Когда вы вызываете WaitOne для AutoResetEvent, поток переходит в спящий режим. Он не будет получать процессорное время, пока не будет установлено событие или не истечет период ожидания. Совершенно очевидно, что какой-то другой поток получает значительный временной интервал и вызывает ложный сбой. Но я думаю, что это маловероятно. У меня есть несколько тестов, которые выполняются таким же образом, и я не получаю очень много / каких-либо ложных сбоев, как это Я обычно выбираю тайм-аут, хотя ~ 2 минуты.

3 голосов
/ 18 февраля 2009

Ждать ручку было бы так, как я бы это сделал. AFAIK (а я точно не знаю) асинхронные методы используют дескрипторы ожидания, чтобы в любом случае запустить этот метод.

Я не уверен, почему вы думаете, что возникнет состояние гонки, если только вы не дадите аномально короткий промежуток времени на вызов WaitOne. Я бы поставил на ожидание 4-5 секунд, чтобы вы точно знали, сломался ли он, и это была не просто гонка.

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

  • Тема 1 - создать дескриптор ожидания
  • Тема 1 - установить ручку ожидания
  • Thread 2 - waitone на ручке ожидания
  • Поток 2 - проходит через ручку ожидания и продолжает выполнение

Даже при нормальном исполнении

  • Тема 1 - создать дескриптор ожидания
  • Thread 2 - waitone на ручке ожидания
  • Тема 1 - установить ручку ожидания
  • Поток 2 - проходит через ручку ожидания и продолжает выполнение

Либо работает правильно, дескриптор ожидания может быть установлен до того, как Thread2 начнет ожидать его, и все будет обработано для вас.

2 голосов
/ 18 февраля 2009

Вот что я сделал в этих ситуациях: создайте сервис, который принимает делегат и выполняет его (предоставьте это через простой интерфейс, введите, где вы вызываете вещи асинхронно).

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

Вот пример:

public interface IActionRunner
{
   void Run(Action action, AsyncCallback callback, object obj);
   void Run<T>(Action<T> action, T arg, AsyncCallback callback, object obj);
   void Run<T1, T2>(Action<T1, T2> action, T1 arg1, T2 arg2, AsyncCallback callback, object obj);
   void Run<T1, T2, T3>(Action<T1, T2, T3> action, T1 arg1, T2 arg2, T3 arg3, AsyncCallback callback, object obj);
   void Run<T1, T2, T3, T4>(Action<T1, T2, T3, T4> action, T1 arg1, T2 arg2, T3 arg3, T4 arg4, AsyncCallback callback, object obj);
}

Асинхронная реализация этого сервиса выглядит следующим образом:

public void Run<T>(Action<T> action, T arg, AsyncCallback callback, object obj)
{
   action.BeginInvoke(arg, callback, obj);
}

Синхронная реализация этого сервиса выглядит так:

public void Run<T>(Action<T> action, T arg, AsyncCallback callback, object obj)
{
   action(arg);
}

Когда вы проводите модульное тестирование, если вы используете что-то вроде RhinoMocks и контейнера AutoMocking, вы можете поменять их в Synchronous ActionRunner:

_mocks = new MockRepository();

_container = new AutoMockingContainer(_mocks);
_container.AddService(typeof(IActionRunner), new SyncActionRunner());
_container.Initialize();

ActionRunner не нуждается в проверке; это всего лишь тонкий шпон над вызовом метода.

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...