2013-09-29 32 views
19

我正在寫我的第一個iOS單元測試(Xcode 5,iOS 6),並發現單元測試的結果因我最近在模擬器中所做的不同而異。例如。我在模擬器的聯繫人列表中單擊一個用戶,現在我的UserDefaults中的「最近的聯繫人」數據比以前多了一個對象,即使我正在運行單元測試。不應該NSUserDefault是單元測試的乾淨版嗎?

對於單元測試,它不是乾淨的隨機用戶默認數據(我習慣了用自己的乾淨數據庫進行RoR測試)。此外,我可能想要測試特定的狀態,比如有空的「近期聯繫人」數據。

從這裏看相關的問題,我似乎有些可能的答案,我不滿意。

  • 模擬UserDefaults的單元測試!我將不得不修改許多現有的類,以便我可以注入這個模擬。
  • 在setUp方法中清除或自定義UserDefaults!但那時在手動測試中辛苦創建的數據將不復存在。
  • 在setUp方法中清除或自定義UserDefaults 然後在tearDown中恢復這些值!哎喲。

這些似乎是不必要的複雜的東西,應該是單元測試的標準做法。我不想在每個單元測試中重複自己。所以,我的問題是:

  • 我是否缺少一些有關UserDefaults從ad-hoc模擬器測試到單元測試運行持續的方式?
  • 是否有一種可配置的方式來解決這個問題,比如說,設置單元測試目標的UserDefaults具有不同的存儲位置的方式要比使用模擬器進行手動測試時的不同?
  • 失敗的是,有沒有一個優雅的方式來做到這一點的代碼?
  • 例如,我可以從XCTestCase繼承一個MyAppTestCase對象,並覆蓋setUp和tearDown方法,以便始終預留並恢復UserDefaults。這是一個好主意嗎?
+0

你不應該單元測試用戶的默認值,因爲這是你沒有直接控制的東西。單元測試應該在原子軟件單元本身上工作 - >不應該涉及其他類或服務。你的方法聽起來更像集成測試。後者通常會使用嘲弄的界面。 – Till

+4

我想你誤解了我的問題直到。如何進行單元測試而不用拉入我無法直接控制的東西?我不打算用實際值拉入用戶默認值。 – LisaD

+0

另一種解決方案是使用像OCMock這樣的隔離框架。添加一個所謂的seam作爲NSUserDefaults類型的屬性。被測試的類將使用存儲在該屬性中的standardUserDefaults進行初始化。在你的測試中,你可以用模擬/存根覆蓋默認對象。 –

回答

18

使用命名套件like in this answer對我很好。刪除用於測試的用戶默認值也可以在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

這是我見過的最接近乾淨的解決方案。太糟糕了,我不再爲iOS編程了! – LisaD

+1

我很想知道爲什麼這個接受的答案沒有任何upvotes,個人。 – Hyperbole

+0

@Hyperbole這個答案比最投票回答年輕2歲。這可能不是最初接受的答案,而且還在追趕上。再給它幾年,它可能是領先的:-) – Benjohn

13

正如@Till所示,您的設計對於良好的可測試性可能是不正確的。他們應該與其他物體(可能與NSUserDefaults交談)一起使用,而不是直接讀取NSUserDefaults系統的單元可測試部分。這大致相當於「嘲諷NSUserDefaults」,但確實是一個額外的抽象層。您的配置對象會抽象NSUserDefaults和其他配置存儲,如鑰匙串。它也可以確保你不會在程序周圍分散字符串常量。我爲許多項目構建了這種配置對象,並強烈推薦它。

有些人會爭辯說,單元可測試的對象不應該依賴單例如NSUserDefaults或我推薦的全局「配置」對象。相反,所有的配置都應該在初始化時注入。在實踐中,我發現這會在與故事板互動時產生太多頭痛,但值得在可能有用的地方考慮。

如果你真的想深入挖掘NSUserDefaults,它確實提供了一些分層功能。您可以調查setVolatileDomain:forName:以查看是否可以爲單元測試創​​建額外的圖層。實際上,我在iOS上沒有太多的運氣(更多的是在Mac上,但還沒達到你需要信任的水平)。

可以調整standardUserDefaults,但如果可以避免,我不會推薦這種方法。如果你不能適應你的設計以避免外部因素,你的「在開始時保存所有內容並將所有內容恢復到最終」可能是解決問題的最佳標準方式。

+1

+1適當和詳細的答案 - 抽象層確實是一種很好的方法。我發現單元測試經常迫使我創建更好的軟件,因爲我不得不以一種允許進行適當的單元測試的方式構建事物 - 儘管這些測試非常有幫助;)。 – Till

+0

我以爲這是一個配置/環境問題,就像它在其他框架中的方式一樣(例如,RoR默認是每次運行都創建一個完全乾淨的數據庫)。如果不這樣做,我同意你添加一個抽象層。謝謝。 – LisaD

+1

在您的項目的某個時間點,單身模式可能會變成強制性的。 http://twobitlabs.com/2011/02/mocking-singletons-with-ocmock/ – Francescu

19

可用的iOS 7/10.9

而不是使用standardUserDefaults你可以使用套件名稱與一些代碼來加載測試

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

這加上從適當的刪除SomeOtherTests.plist文件目錄setUp將歸檔所需的結果。

你將不得不設計任何對象來取你的默認對象,這樣測試就不會有任何副作用。

+0

這是我走過的方法,它工作得很好。我有一個使用用戶默認的服務,它有一個'init'方法,允許用戶默認實例被「注入」。這使得單元測試服務變得很容易。 **要從用戶默認值**清除設置,請使用方法'removePersistentDomainForName:'。這比用plist文件在磁盤上mon easier更容易。 – Benjohn

1

您可以輕鬆地保存&還原主束的標識符,這是什麼[[NSUserDefaults standardUserDefaults] setObject:forKey:]寫入持久域。例如,

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

您也可以使用[[NSUserDefaults standardUserDefaults] volatileDomainForName:NSRegistrationDomain]如果您要訪問的您註冊使用所有-registerDefaults:電話(至少是對於那些已經跑到哪裏單元測試有任何代碼的東西單個組合詞典當然了)。

1

我喜歡創造一個新的,所以沒有勾結。

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")) 
    } 
} 
相關問題