5

In seguito a TDD Sto sviluppando un'app per iPad che scarica alcune informazioni da Internet e le visualizza in un elenco, consentendo all'utente di filtrare tale elenco utilizzando una barra di ricerca.Codice di prova con chiamate dispatch_async

Voglio testare che, quando l'utente digita nella barra di ricerca, la variabile interna con il testo del filtro viene aggiornata, l'elenco filtrato di elementi viene aggiornato e infine la vista tabella riceve un messaggio "reloadData".

Queste sono le mie prove:

- (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]; 
} 

NOTA: La modifica della proprietà "filterText" si innesca in questo momento il processo di filtraggio vero e proprio, che è stato testato in altri test.

Questo funziona male come il mio codice Searchbar delegato è stato scritto come segue:

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

Il problema è che il filtraggio questi dati sta diventando un processo pesante che in questo momento si sta facendo sul thread principale, così in quel tempo in cui l'interfaccia utente è bloccata.

Pertanto, ho pensato di fare qualcosa di simile:

- (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]; 
     }); 
    }); 
} 

In modo che il processo di filtraggio si verifica in un thread diverso e quando ha finito, la tabella viene chiesto di ricaricare i propri dati.

La domanda è ... come posso testare queste cose all'interno delle chiamate dispatch_async?

C'è qualche elegante modo di fare altro che soluzioni temporali? (come aspettare un po 'di tempo e aspettarsi che quei compiti siano finiti, non molto deterministici)

O forse dovrei mettere il mio codice in un modo diverso per renderlo più testabile?

Nel caso sia necessario sapere, sto usando OCMockito e OCHamcrest entro il Jon Reid.

Grazie in anticipo !!

+0

L'utilizzo di brakpoint e NSlog può aiutarti? –

+0

Per quale motivo si hanno i primi due metodi. –

+0

Ciao @ArpitParekh! L'idea sta usando [unit test] (https://en.wikipedia.org/wiki/Unit_testing) per testare automaticamente il mio codice. Non si tratta di trovare un bug, ma di assicurare che questo codice si comporti correttamente da ora in poi. I primi due metodi sono i test della mia suite di test. Controlla il link sul test dell'unità per maggiori informazioni :) – sergiou87

risposta

5

Ci sono due approcci di base. O

  • Rende le cose sincrone solo durante il test. Oppure,
  • Mantieni le cose asincrone, ma scrivi un test di accettazione che esegue la risincronizzazione.

Per rendere le cose sincrone solo per il test, estrarre il codice che effettivamente funziona nei propri metodi. Hai già -filteredDataWithText:. Ecco un altro di estrazione:

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

Il metodo reale che si prende cura di tutto il threading ora assomiglia a questo:

- (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]; 
     }); 
    }); 
} 

Avviso che sotto tutta quella filettatura fanciness, è in realtà solo chiama due metodi.Così ora di far finta che tutto ciò che è stato fatto filettatura, avere i test appena invocano questi due metodi per:

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

Questo significa che -searchBar:textDidChange: non saranno coperti dal test di unità. Un singolo test manuale può confermare che sta spedendo le cose giuste.

Se si desidera realmente un test automatizzato sul metodo delegato, scrivere un test di accettazione che abbia il proprio ciclo di esecuzione. Vedi Pattern for unit testing async queue that calls main queue on completion. (Mantenere i test di accettazione in un target di test separato.)

+0

Grazie Jon! In questo momento sto solo scrivendo test unitari ed è in qualche modo difficile per me prendere la decisione di non coprire alcuni metodi, ma credo che sia quando i test di accettazione vengono in soccorso in casi come questo. – sergiou87

3

Le opzioni di Albite Jons sono opzioni molto buone la maggior parte del tempo, a volte crea un codice meno ingombrante quando si esegue quanto segue. Ad esempio se la tua API ha metodi molto piccoli che sono sincronizzati utilizzando una coda di invio.

Avere una funzione come questa (potrebbe essere anche un metodo della classe).

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

quindi utilizzare questa funzione per richiamare i blocchi nei vostri metodi API

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

Si potrebbe solito inizializzare la coda nel metodo init.

@implementation MyAPI 
{ 
    dispatch_queue_t dispQueue; 
} 

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

    return self; 
} 

Quindi avere un metodo privato come questo, per impostare questa coda su zero. Non fa parte della tua interfaccia, il consumatore dell'API non lo vedrà mai.

- (void) disableGCD 
{ 
    dispQueue = nil; 
} 

Nel vostro obiettivo test che si creare una categoria per esporre il metodo di disattivazione GCD:

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

Si chiama questo nella vostra configurazione di prova e i blocchi sarai chiamato direttamente.

Il vantaggio nei miei occhi è il debug. Quando un caso di test coinvolge un runloop in modo che i blocchi vengano effettivamente chiamati, il problema è che deve esserci un timeout. Questo timeout è solitamente piuttosto breve perché non si desidera che i test durino a lungo se vengono eseguiti nel timeout. Ma avere un breve timeout significa che il test viene eseguito nel timeout durante il debug.

+0

Grazie per la tua risposta! Attualmente sto optando per un'altra soluzione: nascondere il codice asincrono in un'altra classe e prendere in giro quella classe durante il test. Con una spia catturo il blocco di completamento e il mock esegue il blocco di completamento immediatamente. Non c'è più codice asincrono nei miei test :) – sergiou87

Problemi correlati