2013-09-29 11 views
19

Piszę moje pierwsze testy jednostek iOS (Xcode 5, iOS 6) i stwierdzam, że wyniki testów jednostkowych różnią się w zależności od tego, co ostatnio zrobiłem w Symulatorze. Na przykład. Klikam użytkownika na mojej liście kontaktów w Symulatorze, a teraz moje dane "ostatnich kontaktów" w UserDefaults mają jeszcze jeden obiekt niż wcześniej, nawet gdy prowadzę testy jednostkowe.Czy NSUserDefault nie powinien być czystym polem do testów jednostkowych?

Do testów jednostkowych, to nie jest czysta, aby mieć domyślne dane losowe użytkownika (jestem przyzwyczajony do testów RoR z własnej czystej dB). Poza tym, chciałbym przetestować określone stany, takie jak puste dane "ostatnich kontaktów".

od patrzenia na pytania związane z tutaj, wydaje mi się kilka możliwych odpowiedzi, że nie jestem zadowolony.

  • Próbne UserDefaults do testów jednostkowych! Musiałbym zmodyfikować wiele istniejących klas, aby móc wstrzyknąć tę próbę.
  • Wyczyść lub dostosuj UserDefaults w metodzie setUp! Ale wtedy moje dane tworzone pracowicie w ręcznych testach znikną.
  • Pogodne lub dostosować UserDefaults w metodzie setup następnie przywrócić te wartości w przerywaniem! Oooo.

Wydaje się, że są niepotrzebnie skomplikowane z powodu czegoś, co powinno być standardową praktyką w testach jednostkowych. Nie chcę powtarzać się w każdym teście jednostkowym. Tak, moje pytania są:

  • Czy brakuje mi czegoś, co byłoby pożądane w kwestii sposobu utrzymywania UserDefaults z testów symulacyjnych ad-hoc do testów jednostkowych?
  • Czy istnieje konfigurowalny sposób, aby to naprawić, powiedz jakiś sposób, aby ustawić cel testu urządzenia, aby miał inną lokalizację przechowywania dla UserDefaults niż wtedy, gdy użyję symulatora do ręcznego testu?
  • W przypadku braku tego, czy istnieje elegancki sposób na zrobienie tego w kodzie?
  • Na przykład, mogę mieć MyAppTestCase obiektów dziedziczą z XCTestCase i zastąpić metody setup i przerywaniem zawsze uchylenie następnie przywrócić UserDefaults. Czy to dobry pomysł?
+0

Nie powinieneś testować urządzenia domyślnie, ponieważ nie masz nad nim bezpośredniej kontroli. Test jednostkowy powinien działać na samym atomowym zespole programowym -> nie należy angażować żadnych innych klas ani usług. Twoje podejście brzmi bardziej jak test integracji. Te ostatnie zwykle używałyby pozorowanych interfejsów. – Till

+4

Myślę, że źle zrozumiałeś moje pytanie do. W jaki sposób przeprowadzam testy jednostkowe bez wciągania czegoś, nad czym nie mam bezpośredniej kontroli? Nie zamierzałem pobierać wartości domyślnych użytkownika z prawdziwymi wartościami. – LisaD

+0

Innym rozwiązaniem byłoby użycie architektury izolacyjnej, takiej jak OCMock. Dodaj tak zwany szew do swojej klasy jako właściwość typu NSUserDefaults. Testowana klasa zostanie zainicjowana za pomocą standardowegoUżytkownikaDomyślności przechowywanego we wspomnianej właściwości. W twoim teście możesz nadpisać obiekt domyślny za pomocą makiety/kodu pośredniczącego. –

Odpowiedz

18

Używanie nazwanych apartamentów like in this answer działało dobrze dla mnie. Usunięcie domyślnych ustawień użytkownika używanych do testowania można również wykonać w 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

To jest najbliższe czystemu rozwiązaniu, które widziałem. Szkoda, że ​​już nie programuję na iOS! – LisaD

+1

Chciałbym wiedzieć, dlaczego ta zaakceptowana odpowiedź nie ma osobiście żadnych przebojów. – Hyperbole

+0

@Hyperbole Ta odpowiedź jest o 2 lata młodsza od najczęściej wybieranej na odpowiedź. Prawdopodobnie nie była to pierwotnie przyjęta odpowiedź i wciąż gra nadrabianie zaległości. Daj mu jeszcze kilka lat i to może być na czele :-) – Benjohn

13

Zgodnie z sugestią, Twój projekt prawdopodobnie jest nieprawidłowy ze względu na jego dobrą testowalność. Zamiast ręcznego testowania elementów systemu, przeczytaj bezpośrednio NSUserDefaults, powinny one działać z innym obiektem (który może komunikować się z NSUserDefaults). Jest to w przybliżeniu odpowiednik "kpiny NSUserDefaults", ale jest naprawdę dodatkową warstwą abstrakcji. Twój obiekt konfiguracyjny będzie generował zarówno NSUserDefaults, jak i inne pamięci konfiguracji, takie jak keychain. Zapewniłoby to również, że nie rozrzucacie stałych łańcuchowych wokół programu. Zbudowałem tego rodzaju obiekt konfiguracyjny dla wielu projektów i bardzo go polecam.

Niektórzy twierdzą, że obiekty testowalne jednostkowo nie powinny w ogóle polegać na singletonach takich jak NSUserDefaults lub mój polecany globalny obiekt "konfiguracyjny". Zamiast tego całą konfigurację należy wstrzyknąć przy init. W praktyce uważam, że powoduje to zbyt duże bóle głowy podczas interakcji z Storyboardami, ale warto zastanowić się w miejscach, w których może być użyteczny.

Jeśli naprawdę chcesz kopać głębiej NSUserDefaults, robi zapewniają pewne możliwości nakładania warstw. Możesz zbadać setVolatileDomain:forName:, aby sprawdzić, czy możesz utworzyć dodatkową warstwę do testu jednostki. W praktyce nie miałem szczęścia w tego typu sprawach na iOS (więcej - tak na Macu, ale wciąż nie na poziomie, któremu trzeba by zaufać).

Możliwe jest zawrót głowy standardUserDefaults, ale nie poleciłbym tego podejścia, jeśli można tego uniknąć. Twoje "zapisz wszystko na początku i odtwórz wszystko na końcu" to prawdopodobnie najlepiej ustandaryzowany sposób podejścia do problemu, jeśli nie możesz dostosować projektu, aby uniknąć efektów zewnętrznych.

+1

+1 dla właściwej i pełnej odpowiedzi - warstwa abstrakcji jest rzeczywiście czymś, co jest świetnym podejściem. Zauważyłem, że testy jednostkowe często zmuszają mnie do zbudowania znacznie lepszego oprogramowania tylko dlatego, że jestem zmuszony do budowania rzeczy w sposób, który pozwala na właściwe testowanie jednostkowe - pomimo faktu, że te testy są bardzo pomocne;). – Till

+0

Spodziewałem się, że będzie to problem dotyczący konfiguracji/środowiska, tak jak w innych frameworkach (na przykład domyślnie RoR polega na tworzeniu idealnie czystego db w każdym uruchomieniu). W przeciwnym razie zgadzam się z tobą na temat dodania warstwy abstrakcji. dzięki. – LisaD

+1

Swizzle Singleton mogą stać się obowiązkowe w pewnym momencie projektu. http://twobitlabs.com/2011/02/mocking-singletons-with-ocmock/ – Francescu

19

Dostępne iOS 7/10,9

zamiast używać standardUserDefaults można użyć nazwy suite, aby załadować testy

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

To w połączeniu z kodem, aby usunąć plik z odpowiedniej SomeOtherTests.plist katalog w setUp zarchiwizuje pożądany wynik.

Będziesz musiał zaprojektować dowolny obiekt, aby zabrać obiekty domyślne, tak aby nie było żadnych skutków ubocznych z testów.

+0

Jest to podejście, z którym współpracowałem i działa naprawdę dobrze. Mam usługę, która używa wartości domyślnych użytkownika i ma metodę "init", która pozwala na "wstrzyknięcie" instancji domyślnej użytkownika. Ułatwia to jednostkowe przetestowanie usługi. ** Aby usunąć ustawienia z domyślnych ustawień użytkownika **, użyj metody 'removePersistentDomainForName:'. Jest to łatwiejsze niż małpowanie na dysku za pomocą plików plist. – Benjohn

1

można łatwo zapisać & przywrócić trwałą domenę dla identyfikatora głównej wiązki, która jest co [[NSUserDefaults standardUserDefaults] setObject:forKey:] zapisuje. Na przykład,

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

Można również użyć [[NSUserDefaults standardUserDefaults] volatileDomainForName:NSRegistrationDomain] jeśli chcesz mieć dostęp do jednego połączonego słownika rzeczy zarejestrować się przy użyciu wszystkich połączeń -registerDefaults: (przynajmniej dla dowolnego kodu, który jest prowadzony aż do miejsca, gdzie badanie jednostka ma zaczęło się, oczywiście).

1

Lubię tworzyć nowe, więc nie ma zmowy.

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")) 
    } 
} 
Powiązane problemy