Тестовый код с вызовами dispatch_async

Следуя TDD, я разрабатываю приложение для iPad, которое загружает некоторую информацию из Интернета и отображает ее в списке, позволяя пользователю фильтровать этот список с помощью панели поиска.

Я хочу проверить, что по мере того, как пользователь вводит текст в строку поиска, обновляется внутренняя переменная с текстом фильтра, обновляется отфильтрованный список элементов, и, наконец, табличное представление получает сообщение «reloadData».

Это мои тесты:

- (void)testSutChangesFilterTextWhenSearchBarTextChanges
{
    // given
    sut.filterText = @"previous text";

    // when
    [sut searchBar:nil textDidChange:@"new text"];

    // then
    assertThat(sut.filterText, is(equalTo(@"new text")));
}

- (void)testSutReloadsTableViewDataAfterChangeFilterTextFromSearchBar
{
    // given
    sut.tableView = mock([UITableView class]);

    // when
    [sut searchBar:nil textDidChange:@"new text"];

    // then
    [verify(sut.tableView) reloadData];
}

ПРИМЕЧАНИЕ. Изменение свойства "filterText" запускает фактический процесс фильтрации, который был протестирован в других тестах.

Это работает нормально, поскольку мой код делегата searchBar был написан следующим образом:

- (void)searchBar:(UISearchBar *)searchBar textDidChange:(NSString *)searchText
{
    self.filterText = searchText;
    [self.tableView reloadData];
}

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

Поэтому я подумал сделать что-то вроде этого:

- (void)searchBar:(UISearchBar *)searchBar textDidChange:(NSString *)searchText
{
    dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
        NSArray *filteredData = [self filteredDataWithText:searchText];

        dispatch_async(dispatch_get_main_queue(), ^{
            self.filteredData = filteredData;
            [self.tableView reloadData];
        });
    });
}

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

Вопрос в том... как мне протестировать эти вещи внутри вызовов dispatch_async?

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

Или, может быть, я должен поместить свой код по-другому, чтобы сделать его более тестируемым?

Если вам нужно знать, я использую OCMockito и OCHamcrest, автор Джон Рид.

Заранее спасибо!!


person sergiou87    schedule 12.05.2013    source источник
comment
Использование точек останова и NSLogs может вам помочь?   -  person Arpit B Parekh    schedule 12.05.2013
comment
Для какой цели у вас есть первые два метода.   -  person Arpit B Parekh    schedule 12.05.2013
comment
Привет @ArpitParekh ! Идея заключается в использовании модульного тестирования для автоматического тестирования моего кода. Дело не в том, чтобы найти ошибку, а в том, чтобы с этого момента код работал правильно. Первые два метода — это тесты из моего набора тестов. Проверьте ссылку о модульном тестировании для получения дополнительной информации :)   -  person sergiou87    schedule 13.05.2013


Ответы (2)


Есть два основных подхода. Либо

  • Делайте вещи синхронными только во время тестирования. Или,
  • Держите вещи асинхронными, но напишите приемочный тест, который выполняет повторную синхронизацию.

Чтобы сделать вещи синхронными только для тестирования, извлеките код, который действительно работает, в их собственные методы. У вас уже есть -filteredDataWithText:. Вот еще извлечение:

- (void)updateTableWithFilteredData:(NSArray *)filteredData
{
    self.filteredData = filteredData;
    [self.tableView reloadData];
}

Настоящий метод, который заботится обо всех потоках, теперь выглядит так:

- (void)searchBar:(UISearchBar *)searchBar textDidChange:(NSString *)searchText
{
    dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
        NSArray *filteredData = [self filteredDataWithText:searchText];

        dispatch_async(dispatch_get_main_queue(), ^{
            [self updateTableWithFilteredData:filteredData];
        });
    });
}

Обратите внимание, что под всей этой причудливостью потоков он на самом деле просто вызывает два метода. Итак, теперь, чтобы сделать вид, что все потоки выполнены, пусть ваши тесты просто вызывают эти два метода по порядку:

NSArray *filteredData = [self filteredDataWithText:searchText];
[self updateTableWithFilteredData:filteredData];

Это означает, что -searchBar:textDidChange: не будет охватывать модульные тесты. Один ручной тест может подтвердить, что он отправляет правильные вещи.

Если вам действительно нужен автоматизированный тест для метода делегата, напишите приемочный тест с собственным циклом выполнения. См. Шаблон для модульного тестирования асинхронной очереди, вызывает основную очередь по завершении. (Но держите приемочные тесты в отдельной тестовой цели. Они слишком медленные, чтобы включать их в модульные тесты.)

person Jon Reid    schedule 13.05.2013
comment
Спасибо, Джон! Прямо сейчас я просто пишу модульные тесты, и мне как-то сложно принять решение не охватывать некоторые методы, но я думаю, что именно тогда приемочные тесты приходят на помощь в подобных случаях. - person sergiou87; 13.05.2013

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

Имейте такую ​​​​функцию (это может быть и метод вашего класса).

void dispatch(dispatch_queue_t queue, void (^block)())
{
    if(queue)
    {
        dispatch_async(queue, block);
    }
    else
    {
        block();
    }
}

Затем используйте эту функцию для вызова блоков в ваших методах API.

- (void)anAPIMethod
{
    dispatch(dispQueue, ^
    {
        // dispatched code here
    });
}

Обычно вы инициализируете очередь в своем методе init.

@implementation MyAPI
{
    dispatch_queue_t dispQueue;
}

- (instancetype)init
{
    self = [super init];
    if (self)
    {
        dispQueue = dispatch_queue_create("myQueue", DISPATCH_QUEUE_SERIAL);
    }

    return self;
}

Затем используйте такой частный метод, чтобы установить эту очередь в ноль. Это не часть вашего интерфейса, потребитель API никогда этого не увидит.

- (void) disableGCD
{
    dispQueue = nil;
}

В вашей тестовой цели вы создаете категорию, чтобы выставить метод отключения GCD:

@interface TTBLocationBasedTrackStore (Testing)
- (void) disableGCD;
@end

Вы вызываете это в своей тестовой настройке, и ваши блоки будут вызываться напрямую.

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

person sofacoder    schedule 01.09.2015
comment
Спасибо за Ваш ответ! В настоящее время я выбираю другое решение: скрыть асинхронный код в другом классе и издеваться над этим классом во время тестирования. С помощью шпиона я фиксирую блок завершения, а макет просто немедленно выполняет этот блок завершения. В моих тестах больше нет асинхронного кода :) - person sergiou87; 07.09.2015