2016-01-29 20 views
5

Mam klasy, który chcę przetestować za pomocą XCTest, a ta klasa wygląda mniej więcej tak:Mocking Singleton/sharedInstance w Swift

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

Klasa używa pojedyncza który jest zaimplementowany jako to:

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

Teraz do testu. Chcę przetestować, że method() faktycznie robi to, co powinien zrobić, ale nie chcę wywoływać kodu w callMethod() (ponieważ robi straszne rzeczy async/network/thread, które nie powinny działać w testach MyClass, i spowoduje awarię testów).

Więc co ja w zasadzie chciałoby zrobić to w ten sposób:

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

To oczywiście nie jest ważny Swift, ale masz pomysł. Jak mogę przesłonić callMethod() tylko dla tego konkretnego testu, aby uczynić go nieszkodliwym?

EDYCJA: Próbowałem rozwiązać to za pomocą zastrzyku zależności, ale napotkał duże problemy. I stworzył własny startowe metody po prostu być wykorzystywane do badań, takich, że mogę tworzyć swoje obiekty tak:

let myObject = MyClass(singleton: MockSingletonClass) 

i niech MojaKlasa wyglądać następująco

public class MyClass: NSObject { 
    let singleton: SingletonClass 

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

    init() { 
     singleton = SingletonClass.sharedInstance 
    } 

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

Mieszanie w kodzie testowym z resztą kodu jest coś, co wydaje mi się nieco nieprzyjemne, ale w porządku. Dużym problemem było to, że miałem dwa singletons skonstruowany tak w moim projekcie, zarówno przedstawieniu siebie:

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

To stworzyło impasu, kiedy jeden z pojedynczych gdzie pierwszy użyty, jako metodę init, by czekać na init metoda drugiej, i tak dalej ...

Odpowiedz

5

ostatecznie rozwiązać ten przy użyciu kodu

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 
    } 

} 

ten sposób można łatwo drwić moją klasę podczas testów:

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

W testach:

SingletonClass.sharedInstance = MockSingleton() 

Test tylko Kod aktywowany jest przez dodanie -D TEST do "Inne flagi Swift" w Ustawieniach budowania dla celu testu aplikacji.

+3

Okropne rozwiązanie pozwalające na wyciek testów do kodu produkcyjnego – IlyaEremin

4

Twoje singletony podążają za bardzo powszechnym wzorem w bazach kodu Swift/Objective-C. Jest również, jak widzieliście, bardzo trudnym do przetestowania i łatwym sposobem napisania niepoprawnego kodu. Są chwile, kiedy singleton jest użytecznym wzorcem, ale moje doświadczenie było takie, że większość zastosowań wzoru jest w rzeczywistości kiepskim dopasowaniem do potrzeb aplikacji.

Singletor +shared_ styl z Objective-C i Swift klasy stałych pojedynczych zwykle zapewniają dwa problemy:

  1. To może wymusić, że tylko jedno wystąpienie klasy mogą być instancja. (W praktyce często nie jest to egzekwowane i można w dalszym ciągu wykonywać dodatkowe instancje, a aplikacja zależy od deweloperów zgodnych z konwencją wyłącznego dostępu do udostępnionej instancji za pomocą metody klasy.)
  2. Działa jako globalna, zezwalając dostęp do współużytkowanej instancji klasy.

Zachowanie nr 1 jest czasami przydatne, natomiast zachowanie # 2 jest tylko globalne z dyplomem wzoru.

Rozwiązałbym twój konflikt przez całkowite usunięcie globali.Wstrzyknij swoje zależności przez cały czas, zamiast tylko do testowania i zastanów się, jaka odpowiedzialność jest widoczna w Twojej aplikacji, gdy potrzebujesz czegoś, aby skoordynować zestaw współdzielonych zasobów, które wstrzykujesz.

Pierwsze przejście w zależności od wstrzyknięcia w aplikacji jest często bolesne; "ale potrzebuję tego przykładu wszędzie!". Użyj go jako zachęty do ponownego przemyślenia projektu, dlaczego tak wiele komponentów uzyskuje dostęp do tego samego stanu globalnego i jak można go zamiast tego modelować, aby zapewnić lepszą izolację?


Są przypadki, w których chcesz mieć jedną kopię jakiegoś zmiennego stanu zmiennego, a przykład singleton jest prawdopodobnie najlepszą implementacją. Jednak uważam, że w większości przypadków nadal nie jest to prawda. Programiści zazwyczaj szukają współdzielonego stanu, ale z pewnymi warunkami: jest tylko jeden ekran do momentu podłączenia zewnętrznego wyświetlacza, jest tylko jeden użytkownik, dopóki nie wyloguje się na drugie konto, jest tylko jedna kolejka żądań sieciowych, dopóki nie pojawi się potrzeba uwierzytelnienia kontra anonimowe żądania. Podobnie często chcesz mieć udostępnioną instancję do czasu wykonania następnego przypadku testowego.

Biorąc pod uwagę, jak mało "singletonów" wydaje się używać inicjalizatorów failable (lub metod inicjalizacji obj-c, które zwracają istniejącą wspólną instancję) wydaje się, że programiści chętnie dzielą ten stan zgodnie z konwencją, więc nie widzę powodu, aby nie wstrzykiwać obiekt współdzielony i zapisuj łatwo sprawdzalne klasy zamiast używać globali.

0

Miałem podobny problem w mojej aplikacji, aw moim przypadku sensowne było używanie Singletonów dla tych konkretnych usług, ponieważ były one serwerami proxy dla usług zewnętrznych, które również były pojedynczymi.

Skończyłem na wdrażaniu modelu Dependency Injection przy użyciu https://github.com/Swinject/Swinject. Zajęło to około jednego dnia wdrożenie na około 6 klas, co wydaje się dużo czasu na umożliwienie tego poziomu testowalności jednostki. Zmusiło mnie to do zastanowienia się nad zależnościami pomiędzy moimi klasami usług, co sprawiło, że bardziej wyraźnie wskazałem je w definicjach klas. Użyłem ObjectScope w Swinject, aby wskazać DI framework, że są singletonami: https://github.com/Swinject/Swinject/blob/master/Documentation/ObjectScopes.md

Udało mi się uzyskać moje singletony i przekazać je w wersji próbnej na moje testy jednostkowe.

Myśli o tym podejściu: wydaje się bardziej podatny na błędy, ponieważ mogłem zapomnieć o pomyślnym zainicjowaniu moich zależności (i nigdy nie otrzymam błędu runtime). Wreszcie, nie uniemożliwia to bezpośredniej instancji instancji mojej klasy Service (co stanowiło cały punkt singletonu), ponieważ moje metody init musiały zostać upublicznione dla DI Framework, aby utworzyć instancje obiektów do rejestr.