2016-01-29 17 views
5

Ho una classe che voglio mettere alla prova con XCTest, e questa classe simile a questa:Mocking Singleton/sharedInstance a Swift

public class MyClass: NSObject { 
    func method() { 
     // Do something... 
     // ... 
     SingletonClass.sharedInstance.callMethod() 
    } 
} 

La classe utilizza un Singleton che viene implementato come questo:

public class SingletonClass: NSObject { 

    // Only accessible using singleton 
    static let sharedInstance = SingletonClass() 

    private override init() { 
     super.init() 
    } 

    func callMethod() { 
     // Do something crazy that shouldn't run under tests 
    } 
} 

Ora per il test. Voglio testare che method() fa effettivamente quello che dovrebbe fare, ma non voglio richiamare il codice in callMethod() (perché fa cose orribili asincrone/rete/thread che non dovrebbero essere eseguite sotto test di MyClass, e farà crash i test).

Quindi quello che fondamentalmente vorrei fare è questo:

SingletonClass = MockSingletonClass: SingletonClass { 
    override func callMethod() { 
     // Do nothing 
    } 
let myObject = MyClass() 
myObject.method() 
// Check if tests passed 

Questo ovviamente non è valido Swift, ma si ottiene l'idea. Come posso ignorare lo callMethod() solo per questo particolare test, per renderlo innocuo?

MODIFICA: Ho provato a risolverlo utilizzando una forma di dipendenza, ma ho riscontrato grossi problemi. Ho creato un init-metodo personalizzato solo per essere utilizzato per le prove in modo che avrei potuto creare i miei oggetti come questo:

let myObject = MyClass(singleton: MockSingletonClass) 

e lasciare MyClass simile a questa

public class MyClass: NSObject { 
    let singleton: SingletonClass 

    init(mockSingleton: SingletonClass){ 
     self.singleton = mockSingleton 
    } 

    init() { 
     singleton = SingletonClass.sharedInstance 
    } 

    func method() { 
     // Do something... 
     // ... 
     singleton.callMethod() 
    } 
} 

miscelazione in codice di test con il resto del codice è qualcosa che trovo un po 'spiacevole, ma va bene. Il grosso problema era che avevo due singletons costruite come questo nel mio progetto, sia riferimento a vicenda:

public class FirstSingletonClass: NSObject { 
    // Only accessible using singleton 
    static let sharedInstance = FirstSingletonClass() 

    let secondSingleton: SecondSingletonClass 

    init(mockSingleton: SecondSingletonClass){ 
     self.secondSingleton = mockSingleton 
    } 

    private override init() { 
     secondSingleton = SecondSingletonClass.sharedInstance 
     super.init() 
    } 

    func someMethod(){ 
     // Use secondSingleton 
    } 
} 

public class SecondSingletonClass: NSObject { 
    // Only accessible using singleton 
    static let sharedInstance = SecondSingletonClass() 

    let firstSingleton: FirstSingletonClass 

    init(mockSingleton: FirstSingletonClass){ 
     self.firstSingleton = mockSingleton 
    } 

    private override init() { 
     firstSingleton = FirstSingletonClass.sharedInstance 
     super.init() 
    } 

    func someOtherMethod(){ 
     // Use firstSingleton 
    } 
} 

Questo ha creato una situazione di stallo in cui uno dei single in cui utilizzate prima, come il metodo init avrebbe aspettato per l'init metodo dell'altro, e così via ...

risposta

5

alla fine ho risolto questo utilizzando il codice

class SingletonClass: NSObject { 

    #if TEST 

    // Only used for tests 
    static var sharedInstance: SingletonClass! 

    // Public init 
    override init() { 
     super.init() 
    } 

    #else 

    // Only accessible using singleton 
    static let sharedInstance = SingletonClass() 

    private override init() { 
     super.init() 
    } 

    #endif 

    func callMethod() { 
     // Do something crazy that shouldn't run under tests 
    } 

} 

In questo modo posso facilmente deridere la mia classe durante le prove:

private class MockSingleton : SingletonClass { 
    override callMethod() {} 
} 

Nei test:

SingletonClass.sharedInstance = MockSingleton() 

Il test-only il codice viene attivato aggiungendo -D TEST a "Altri Swift Flags" in Impostazioni di costruzione per la destinazione del test dell'app.

+3

Soluzione orribile per lasciare che le prove perdano nel codice di produzione – IlyaEremin

4

I tuoi singleton seguono uno schema molto comune nelle basi di codice Swift/Objective-C. È anche, come hai visto, molto difficile da testare e un modo semplice per scrivere codice non testabile. Ci sono momenti in cui un singleton è un pattern utile ma la mia esperienza è stata che la maggior parte degli usi del pattern sono in realtà inadeguati per le esigenze dell'app.

Il singleton +shared_ stile da Objective-C e di classe Swift singletons costanti di solito forniscono due comportamenti:

  1. Potrebbe valere che solo una singola istanza di una classe può essere istanziata. (In pratica questo spesso non viene applicato e puoi continuare a alloc/init istanze aggiuntive e l'app dipende invece dagli sviluppatori che seguono una convenzione di accesso esclusivo a un'istanza condivisa tramite il metodo di classe.)
  2. Funziona come globale, consentendo accesso a un'istanza condivisa di una classe.

Il comportamento n. 1 è occasionalmente utile mentre il comportamento n. 2 è solo un modello globale con un diploma di modello di progettazione.

Risolvo il conflitto rimuovendo completamente i globali.Inietti le tue dipendenze tutto il tempo invece che solo per i test e considera quale responsabilità esporre nella tua app quando hai bisogno di qualcosa per coordinare qualsiasi insieme di risorse condivise che stai iniettando.

Un primo passaggio alle dipendenze per iniezioni in un'applicazione è spesso doloroso; "ma ho bisogno di questa istanza ovunque!". Utilizzalo come suggerimento per riconsiderare la progettazione, perché così tanti componenti accedono allo stesso stato globale e come potrebbero essere modellati per fornire un migliore isolamento?


Ci sono casi in cui si desidera una singola copia di uno stato condiviso mutabile e un'istanza singleton è forse l'implementazione migliore. Tuttavia trovo che nella maggior parte degli esempi ciò non sia ancora vero. Gli sviluppatori sono in genere alla ricerca di uno stato condiviso ma con alcune condizioni: c'è una sola schermata fino a quando non è connesso un display esterno, c'è un solo utente fino a che non si disconnettono e in un secondo account, c'è solo una coda di richieste di rete finché non si trova la necessità di autenticarsi vs richieste anonime. Allo stesso modo spesso si desidera un'istanza condivisa fino all'esecuzione del prossimo caso di test.

Dato che pochi "singleton" sembrano usare gli inizializzatori failable (oi metodi init di obj-c che restituiscono un'istanza condivisa esistente) sembra che gli sviluppatori siano felici di condividere questo stato per convenzione, quindi non vedo alcun motivo per non iniettare l'oggetto condiviso e scrivere classi prontamente testabili invece di utilizzare globals.

0

Ho avuto un problema simile nella mia app, e nel mio caso aveva senso usare Singletons per questi particolari servizi in quanto erano proxy per servizi esterni che erano anche singleton.

Ho finito per implementare un modello Dipendenza Iniezione utilizzando https://github.com/Swinject/Swinject. Ci sono voluti circa un giorno per implementare circa 6 classi che sembrano un sacco di tempo per abilitare questo livello di testabilità dell'unità. Mi ha fatto riflettere sulle dipendenze tra le mie classi di servizio, e mi ha fatto indicare in modo più esplicito queste definizioni nelle classi. Ho usato l'ObjectScope in Swinject per indicare al framework DI che sono singleton: https://github.com/Swinject/Swinject/blob/master/Documentation/ObjectScopes.md

Sono stato in grado di avere i miei singleton e di trasmetterli in versione fittizia ai miei test di unità.

Riflessioni su questo approccio: sembra più soggetto a errori in quanto potrei dimenticare di inizializzare correttamente le mie dipendenze (e non riceverei mai un errore di runtime). Infine, non impedisce a qualcuno di istanziare direttamente un'istanza della mia classe Service direttamente (che era una specie dell'intero punto del singleton), dal momento che i miei metodi init dovevano essere resi pubblici per il DI Framework per istanziare gli oggetti nel registro di sistema.