Тестовый код с вызовами 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 от Jon Reid.
Заранее спасибо!!
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:
не будут охвачены юнит-тестами. Один ручной тест может подтвердить, что он отправляет нужные вещи.
Если вы действительно хотите автоматизированный тест для метода делегата, напишите приемочный тест, который имеет свой собственный цикл выполнения. См. Шаблон для модульного тестирования асинхронной очереди, которая вызывает основную очередь по завершении. (Но держите приемочные тесты в отдельной тестовой цели. Они слишком медленные, чтобы включать их в модульные тесты.)
Опции 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;
}
Затем используйте приватный метод, подобный этому, чтобы установить для этой очереди значение nil. Он не является частью вашего интерфейса, пользователь API никогда не увидит этого.
- (void) disableGCD
{
dispQueue = nil;
}
В вашей тестовой цели вы создаете категорию для показа метода отключения GCD:
@interface TTBLocationBasedTrackStore (Testing)
- (void) disableGCD;
@end
Вы вызываете это в своей тестовой настройке, и ваши блоки будут вызываться напрямую.
Преимущество в моих глазах - отладка. Когда в тестовом примере задействован цикл запуска, так что блоки фактически вызываются, проблема заключается в том, что должен быть задействован тайм-аут. Этот тайм-аут обычно довольно короткий, потому что вы не хотите, чтобы тесты продолжались долго, если они не превышают тайм-аут. Но наличие короткого таймаута означает, что при отладке ваш тест переходит в таймаут.