Очікуйте виклику методу Async Void для модульного тестування


77

У мене є метод, який виглядає так:

private async void DoStuff(long idToLookUp)
{
    IOrder order = await orderService.LookUpIdAsync(idToLookUp);   

    // Close the search
    IsSearchShowing = false;
}    

//Other stuff in case you want to see it
public DelegateCommand<long> DoLookupCommand{ get; set; }
ViewModel()
{
     DoLookupCommand= new DelegateCommand<long>(DoStuff);
}    

Я намагаюся модульно протестувати це так:

[TestMethod]
public void TestDoStuff()
{
    //+ Arrange
    myViewModel.IsSearchShowing = true;

    // container is my Unity container and it setup in the init method.
    container.Resolve<IOrderService>().Returns(orderService);
    orderService = Substitute.For<IOrderService>();
    orderService.LookUpIdAsync(Arg.Any<long>())
                .Returns(new Task<IOrder>(() => null));

    //+ Act
    myViewModel.DoLookupCommand.Execute(0);

    //+ Assert
    myViewModel.IsSearchShowing.Should().BeFalse();
}

Моє твердження викликається до того, як я закінчу з макетованим LookUpIdAsync. У моєму звичайному коді я саме цього хочу. Але для мого модульного тесту я цього не хочу.

Я переходжу до Async / Await з використанням BackgroundWorker. У фоновому режимі це працювало правильно, оскільки я міг зачекати, поки закінчиться BackgroundWorker.

Але, здається, немає способу дочекатися асинхронного методу void ...

Як я можу протестувати цей метод?

Відповіді:


69

Вам слід уникати async void. Використовувати лише async voidдля обробників подій. DelegateCommand(логічно) обробник подій, тому ви можете зробити це так:

// Use [InternalsVisibleTo] to share internal methods with the unit test project.
internal async Task DoLookupCommandImpl(long idToLookUp)
{
  IOrder order = await orderService.LookUpIdAsync(idToLookUp);   

  // Close the search
  IsSearchShowing = false;
}

private async void DoStuff(long idToLookUp)
{
  await DoLookupCommandImpl(idToLookup);
}

і одиничний тест, як:

[TestMethod]
public async Task TestDoStuff()
{
  //+ Arrange
  myViewModel.IsSearchShowing = true;

  // container is my Unity container and it setup in the init method.
  container.Resolve<IOrderService>().Returns(orderService);
  orderService = Substitute.For<IOrderService>();
  orderService.LookUpIdAsync(Arg.Any<long>())
              .Returns(new Task<IOrder>(() => null));

  //+ Act
  await myViewModel.DoLookupCommandImpl(0);

  //+ Assert
  myViewModel.IsSearchShowing.Should().BeFalse();
}

Моя рекомендована відповідь - вище. Але якщо ви дійсно хочете протестувати async voidметод, ви можете зробити це за допомогою моєї бібліотеки AsyncEx :

[TestMethod]
public void TestDoStuff()
{
  AsyncContext.Run(() =>
  {
    //+ Arrange
    myViewModel.IsSearchShowing = true;

    // container is my Unity container and it setup in the init method.
    container.Resolve<IOrderService>().Returns(orderService);
    orderService = Substitute.For<IOrderService>();
    orderService.LookUpIdAsync(Arg.Any<long>())
                .Returns(new Task<IOrder>(() => null));

    //+ Act
    myViewModel.DoLookupCommand.Execute(0);
  });

  //+ Assert
  myViewModel.IsSearchShowing.Should().BeFalse();
}

Але це рішення змінює модель SynchronizationContextвашого перегляду протягом усього життя.


Це могло би спрацювати. Але я волів би не мати двох методів для всіх своїх асинхронних порожнеч. Я придумав спосіб це зробити (принаймні в моєму випадку). Дивіться мою відповідь на це питання, якщо вам цікаво.
Ваккано

Приємно - ви створили попередник C # github.com/btford/zone.js ZoneJS.
Йохен ван Вілік

Яка перевага очікування DoLookupCommandImpl (idToLookup) у DoStuff (long idToLookUp)? Що робити, якщо його буде викликано без очікування?
jacekbe

2
@jacekbe: awaitпри виконанні завдання спостерігаються винятки; якщо ви викликаєте його без await, то будь-які помилки мовчки ігноруються.
Стівен Клірі

59

async voidМетод, за суті , «вогонь і забути» метод. Немає засобів повернути подію завершення (без зовнішньої події тощо).

Якщо вам потрібно юніт-тестувати це, я б радив async Taskзамість цього зробити метод. Потім ви можете зателефонувати Wait()за результатами, які повідомить вас, коли метод завершиться.

Однак цей метод тестування, як написано, все одно не буде працювати, оскільки ви фактично не тестуєте DoStuffбезпосередньо, а скоріше тестуєте, DelegateCommandякий його обгортає. Вам доведеться протестувати цей метод безпосередньо.


1
Я не можу змінити його, щоб повернути Завдання, оскільки DelegateCommand не дозволяє цього.
Ваккано

Наявність модульних тестових лісів навколо мого коду дуже важливо. Можливо, мені доведеться змусити всі (важливі) методи "async void" використовувати BackgroundWorker, якщо їх неможливо протестувати.
Ваккано

1
@Vaccano Те ж саме відбулося б і з BackgroundWorker - вам просто потрібно зробити це async Taskзамість async voidі почекати виконання завдання ...
Рід Копсі,

@Vaccano У вас не повинно бути async voidметодів (крім обробників подій). Якщо в async voidметоді встановлено виняток , як ви збираєтесь його обробляти?
svick

Я придумав спосіб змусити це працювати (принаймні для цього випадку). Дивіться мою відповідь на це питання, якщо вам цікаво.
Ваккано

18

Я придумав спосіб зробити це для модульного тестування:

[TestMethod]
public void TestDoStuff()
{
    //+ Arrange
    myViewModel.IsSearchShowing = true;

    // container is my Unity container and it setup in the init method.
    container.Resolve<IOrderService>().Returns(orderService);
    orderService = Substitute.For<IOrderService>();

    var lookupTask = Task<IOrder>.Factory.StartNew(() =>
                                  {
                                      return new Order();
                                  });

    orderService.LookUpIdAsync(Arg.Any<long>()).Returns(lookupTask);

    //+ Act
    myViewModel.DoLookupCommand.Execute(0);
    lookupTask.Wait();

    //+ Assert
    myViewModel.IsSearchShowing.Should().BeFalse();
}

Ключ тут полягає в тому, що, оскільки я тестую модуль, я можу замінити у завданні, яке хочу, щоб мій асинхронний виклик (всередині моєї асинхронної порожнечі) повернувся. Потім я просто переконуюсь, що завдання виконано, перш ніж рухатися далі.


Те, що ваш результат lookupTaskзакінчено, не означає, що тестований метод ( DoStuff? Або DoLookupCommand?) Закінчив роботу. Існує невелика ймовірність того, що завдання завершилось, але IsSearchShowingще не було встановлено значення false, і в цьому випадку ваше твердження зазнає невдачі.
dcastro

Простий спосіб довести це - поставити Thread.Sleep(2000)перед тим, як встановити IsSearchShowing значення false.
dcastro


5

Ви можете використовувати AutoResetEvent, щоб зупинити метод тестування, поки не завершиться виклик асинхронізації:

[TestMethod()]
public void Async_Test()
{
    TypeToTest target = new TypeToTest();
    AutoResetEvent AsyncCallComplete = new AutoResetEvent(false);
    SuccessResponse SuccessResult = null;
    Exception FailureResult = null;

    target.AsyncMethodToTest(
        (SuccessResponse response) =>
        {
            SuccessResult = response;
            AsyncCallComplete.Set();
        },
        (Exception ex) =>
        {
            FailureResult = ex;
            AsyncCallComplete.Set();
        }
    );

    // Wait until either async results signal completion.
    AsyncCallComplete.WaitOne();
    Assert.AreEqual(null, FailureResult);
}

будь-який зразок класу AsyncMethodToTest?
Kiquenet

2
Чому б просто не використовувати Wait ()?
Teoman shipahi

5

Надана відповідь перевіряє команду, а не метод async. Як згадувалося вище, вам знадобиться ще один тест, щоб перевірити і цей метод асинхронізації.

Провівши деякий час із подібною проблемою, я знайшов легке очікування, щоб протестувати метод асинхронізації в модульному тесті, просто зателефонувавши синхронно:

    protected static void CallSync(Action target)
    {
        var task = new Task(target);
        task.RunSynchronously();
    }

та використання:

CallSync(() => myClass.MyAsyncMethod());

Тест чекає на цьому рядку і продовжується після того, як результат буде готовий, щоб ми могли стверджувати відразу після цього.


2

Змініть метод, щоб повернути завдання, і ви можете використовувати Task.Result

bool res = configuration.InitializeAsync(appConfig).Result;
Assert.IsTrue(res);

що це configuration?
Рафаель Герсковічі,

1
@Dementic Приклад? Або більш конкретний екземпляр об’єкта з членом InitializeAsync, який повертає завдання.
Біллі Джейк О'Коннор,

0

У мене була подібна проблема. У моєму випадку рішення було використовувати Task.FromResultу налаштуваннях moq приблизно .Returns(...)так:

orderService.LookUpIdAsync(Arg.Any<long>())
    .Returns(Task.FromResult(null));

Як варіант, Moq також має ReturnsAysnc(...)метод.

Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.