5

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

Я хочу проверить, что, когда пользователь вводит строку поиска, обновляется внутренняя переменная с текстом фильтра, фильтрованный список элементов обновляется, и, наконец, представление таблицы получает сообщение «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.

Спасибо заранее!

+0

ИСПОЛЬЗОВАНИЕ brakpoints и NSLogs могут вам помочь? –

+0

Для чего у вас есть первые два метода. –

+0

Привет @ArpitParekh! Идея заключается в использовании [unit testing] (https://en.wikipedia.org/wiki/Unit_testing) для автоматического тестирования моего кода. Речь идет не об обнаружении ошибки, а о том, что этот код ведет себя правильно с этого момента. Первые два метода - это тесты из моего набора тестов. Проверьте ссылку на модульное тестирование для получения дополнительной информации :) – sergiou87

ответ

5

Существует два основных подхода. Либо

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

Чтобы сделать вещи синхронными только для тестирования, извлеките код, который действительно работает в их собственных методах. У вас уже есть -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: не будет покрыта юнит-тестов. Один ручной тест может подтвердить, что он отправляет правильные вещи.

Если вы действительно хотите автоматизированный тест по методу делегата, напишите приемочный тест, который имеет свой собственный цикл запуска. См. Pattern for unit testing async queue that calls main queue on completion. (Но продолжайте приемочные испытания в отдельной тестовой задаче. Они слишком медленны, чтобы включать в модульные тесты.)

+0

Спасибо Jon! Сейчас я просто пишу модульные тесты, и мне как-то трудно принять решение о том, чтобы не освещать некоторые методы, но я думаю, что когда приемочные тесты приходят на помощь в таких случаях. – sergiou87

3

Параметры 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 
    }); 
} 

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

@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; 
} 

В тестовой цели создать категорию, чтобы разоблачить метод отключающий НОД:

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

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

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

+0

Спасибо за ваш ответ! В настоящее время я выбираю другое решение: скройте асинхронный код в другом классе и издевайтесь над этим классом во время тестирования. С шпионом я захватываю блок завершения, и макет просто выполняет этот блок завершения немедленно. В моих тестах больше нет асинхронного кода :) – sergiou87

 Смежные вопросы

  • Нет связанных вопросов^_^