2013-09-29 7 views
19

Sto scrivendo i miei primi test dell'unità iOS (Xcode 5, iOS 6) e ho riscontrato che i risultati dei test delle unità variano a seconda di ciò che ho fatto ultimamente nel Simulatore. Per esempio. Clicco su un utente nel mio elenco di contatti nel simulatore, e ora i miei dati "contatti recenti" in UserDefaults hanno un oggetto in più rispetto a prima, anche quando eseguo test unitari.Non dovrebbe NSUserDefault essere una lavagna pulita per i test unitari?

Per il test delle unità, non è pulito avere dati di default dell'utente casuali (sono abituato ai test RoR con il proprio db pulito). Inoltre, potrei voler testare stati specifici come avere dati "contatti recenti" vuoti.

Dall'esaminare le domande correlate qui, mi sembrano alcune possibili risposte di cui non sono contento.

  • Mock UserDefaults per i test di unità! Dovrei modificare molte classi esistenti in modo da poter iniettare quella finta.
  • Cancella o personalizza UserDefaults in un metodo setUp! Ma poi i miei dati creati faticosamente nei test manuali sarebbero andati.
  • Cancella o personalizza UserDefaults in un metodo setUp quindi ripristina quei valori in tearDown! Ahia.

Questi sembrano inutilmente complicati per qualcosa che dovrebbe essere una pratica standard nei test unitari. Non voglio ripetermi in ogni test unitario. Quindi, le mie domande sono:

  • Mi manca qualcosa di desiderabile sul modo in cui gli UserDefaults vengono mantenuti dal test del Simulatore ad-hoc fino alle unit test runs?
  • Esiste un modo configurabile per risolvere questo problema, ad esempio un modo per impostare l'obiettivo del test dell'unità in modo che disponga di un percorso di memorizzazione diverso per UserDefaults rispetto a quando utilizzo il simulatore per eseguire il test manuale?
  • In caso contrario, c'è un modo elegante per farlo in codice?
  • Ad esempio, potrei avere un oggetto MyAppTestCase ereditato da XCTestCase e sovrascrivere i metodi setUp e tearDown per sempre mettere da parte quindi ripristinare UserDefaults. E 'questa una buona idea?
+0

Non dovresti testare le impostazioni predefinite dell'utente in quanto è qualcosa su cui non hai controllo diretto. Un test unitario dovrebbe funzionare su un'unità software atomica stessa -> non dovrebbero essere coinvolte altre classi o servizi. Il tuo approccio sembra molto più simile a un test di integrazione. Quest'ultimo userebbe comunemente le interfacce mocked-up. – Till

+4

Penso che tu abbia frainteso la mia domanda. COME faccio i test unitari senza inserire qualcosa su cui non ho il controllo diretto? Non intendevo inserire le impostazioni predefinite dell'utente con valori reali. – LisaD

+0

Un'altra soluzione potrebbe essere l'utilizzo di un framework di isolamento come OCMock. Aggiungi una cosiddetta cucitura alla tua classe come una proprietà di tipo NSUserDefaults. La classe in prova verrebbe inizializzata con standardUserDefaults memorizzati in detta proprietà. Nel tuo test puoi sovrascrivere l'oggetto defaults con un mock/stub. –

risposta

18

L'utilizzo di suite denominate like in this answer ha funzionato bene per me. La rimozione delle impostazioni predefinite dell'utente utilizzate per il test potrebbe essere eseguita anche in func tearDown().

class MyTest : XCTestCase { 
    var userDefaults: UserDefaults? 
    let userDefaultsSuiteName = "TestDefaults" 

    override func setUp() { 
     super.setUp() 
     UserDefaults().removePersistentDomain(forName: userDefaultsSuiteName) 
     userDefaults = UserDefaults(suiteName: userDefaultsSuiteName) 
    } 
} 
+2

Questa è la soluzione più pulita che abbia mai visto. Peccato che non sto più programmando per iOS! – LisaD

+1

Mi piacerebbe sapere perché questa risposta accettata non ha alcun upvotes, personalmente. – Hyperbole

+0

@Hyperbole Questa risposta è 2 anni più giovane della risposta più votata. Probabilmente non è stata la risposta originale accettata e sta ancora recuperando terreno. Dagli ancora un paio di anni e potrebbe essere in testa :-) – Benjohn

13

Come suggerisce @Till, il progetto è probabilmente errato per una buona testabilità. Piuttosto che disporre di pezzi del sistema verificabili unitariamente, leggere direttamente NSUserDefaults, dovrebbero funzionare con qualche altro oggetto (che può parlare con NSUserDefaults). Questo è più o meno equivalente a "beffardo NSUserDefaults", ma è davvero un livello di astrazione extra. Il tuo oggetto di configurazione avrebbe astratto sia NSUserDefaults che altra memoria di configurazione come il portachiavi. Garantirebbe inoltre di non disperdere costanti di stringa attorno al programma. Ho costruito questo tipo di oggetto di configurazione per molti progetti e lo consiglio vivamente.

Alcuni sostengono che gli oggetti testabili sull'unità non dovrebbero fare affidamento su singoletti come NSUserDefaults o sul mio oggetto di configurazione globale "configurazione". Invece, tutta la configurazione dovrebbe essere iniettata su init. In pratica, trovo questo per creare troppo mal di testa quando si interagisce con gli Storyboard, ma vale la pena considerare in luoghi in cui può essere utile.

Se si vuole veramente scavare a fondo in NSUserDefaults, fornisce alcune funzionalità di stratificazione. È possibile esaminare setVolatileDomain:forName: per vedere se è possibile creare un livello aggiuntivo per il test dell'unità. In pratica, non ho avuto molta fortuna con questo genere di cose su iOS (molto di più su Mac, ma ancora non al livello di cui avresti bisogno per crederci).

È possibile swizzle standardUserDefaults, ma non consiglierei questo approccio se è possibile evitarlo. Il tuo "salva tutto all'inizio e ripristina tutto alla fine" è probabilmente il modo migliore standardizzato per affrontare il problema se non riesci ad adattare il tuo design per evitare esternalità.

+1

+1 per una risposta corretta e dettagliata - il livello di astrazione è davvero un ottimo approccio. Ho trovato test unitari che spesso mi costringono a costruire software molto migliori solo perché sono costretto a costruire le cose in modo da consentire un corretto test delle unità, nonostante il fatto che quei test siano piuttosto utili;). – Till

+0

Mi aspettavo che questo fosse un problema di configurazione/ambiente così come è in altri framework (ad esempio, RoR default è quello di creare un db perfettamente pulito ad ogni esecuzione). In caso contrario, sono d'accordo con te sull'aggiunta di un livello di astrazione. Grazie. – LisaD

+1

Swizzle Singleton potrebbe diventare obbligatorio in alcuni punti del progetto. http://twobitlabs.com/2011/02/mocking-singletons-with-ocmock/ – Francescu

19

Disponibile iOS 7/10,9

Invece di utilizzare i standardUserDefaults è possibile utilizzare un nome interno per caricare il tuo test

[[NSUserDefaults alloc] initWithSuiteName:@"SomeOtherTests"]; 

Questa accoppiata con un certo codice per rimuovere il file SomeOtherTests.plist dal appropriata directory in setUp archivierà il risultato desiderato.

Dovresti progettare qualsiasi oggetto per prendere gli oggetti di default in modo che non ci siano effetti collaterali dai test.

+0

Questo è l'approccio con cui sono andato e funziona davvero bene. Ho un servizio che utilizza le impostazioni predefinite dell'utente e ha un metodo 'init' che consente all'istanza di default dell'utente di essere" iniettata ". Ciò semplifica il collaudo del servizio. ** Per eliminare le impostazioni dai valori predefiniti dell'utente **, utilizzare il metodo 'removePersistentDomainForName:'. Questo è più semplice di una monkeying sul disco con file plist. – Benjohn

1

Si può facilmente salvare & ripristinare il dominio persistente per l'identificatore del fascio principale, che è quello che [[NSUserDefaults standardUserDefaults] setObject:forKey:] scrive a. Ad esempio,

NSUserDefaults *defaults = [NSUserDefaults standardUserDefaults]; 
NSDictionary *originalValues = [defaults persistentDomainForName:[[NSBundle mainBundle] bundleIdentifier]]; 

// do stuff, possibly [defaults removePersistentDomainForName:[[NSBundle mainBundle] bundleIdentifier]] 
// or using setPersistentDomain: to substitute a dictionary of mock values and test against that 

[defaults setPersistentDomain:originalValues forName:[[NSBundle mainBundle] bundleIdentifier]]; 

È inoltre possibile utilizzare [[NSUserDefaults standardUserDefaults] volatileDomainForName:NSRegistrationDomain] se si desidera accedere a un unico dizionario combinato della roba che si registra utilizzando tutte le -registerDefaults: chiamate (almeno per qualsiasi codice che ha eseguito fino al punto in cui il test di unità ha iniziato, ovviamente).

1

Mi piace creare un nuovo così non c'è collusione.

import XCTest 

extension UserDefaults { 
    private static var index = 0 
    static func createCleanForTest(label: StaticString = #file) -> UserDefaults { 
     index += 1 
     let suiteName = "UnitTest-UserDefaults-\(label)-\(index)" 
     UserDefaults().removePersistentDomain(forName: suiteName) 
     return UserDefaults(suiteName: suiteName)! 
    } 
} 

class MyTest: XCTestCase { 

    func testOne() { 
     let userDefaults = UserDefaults.createCleanForTest() 
     XCTAssertFalse(userDefaults.bool(forKey: "foo")) 
     userDefaults.set(true, forKey: "foo") 
     XCTAssertTrue(userDefaults.bool(forKey: "foo")) 
    } 

    func testTwo() { 
     let userDefaults = UserDefaults.createCleanForTest() 
     XCTAssertFalse(userDefaults.bool(forKey: "foo")) 
     userDefaults.set(true, forKey: "foo") 
     XCTAssertTrue(userDefaults.bool(forKey: "foo")) 
    } 
}