2014-01-07 27 views
10

我使用XCTestOCMock爲iOS應用程序編寫單元測試,並且我需要指導如何最佳設計單元測試,以驗證方法是否導致NSTimer開始。被測編寫單元測試以驗證NSTimer已啓動

代碼:

- (void)start { 
    ... 
    self.timer = [NSTimer timerWithTimeInterval:1.0 
             target:self 
             selector:@selector(tick:) 
             userInfo:nil 
             repeats:YES]; 
    NSRunLoop *runLoop = [NSRunLoop currentRunLoop]; 
    [runLoop addTimer:self.timer forMode:NSDefaultRunLoopMode]; 
    ... 
} 

我要測試的是定時器用正確的參數創建,計時器定於運行循環運行。

我想過的,我不是很滿意下列選項:

  1. 其實等待計時器火災。 (原因我不喜歡它:可怕的單元測試練習,它更像是一個慢速集成測試。)
  2. 將計時器啓動代碼提取到私有方法中,將類擴展文件中的私有方法暴露給單元測試,並使用模擬期望來驗證私有方法被調用。 (原因我不喜歡它:它驗證方法被調用,但不是該方法實際設置計時器正確運行。另外,公開私有方法並不是一個好習慣。)
  3. 提供模擬NSTimer到被測試的代碼。 (原因我不喜歡它:無法驗證它實際上計劃運行,因爲定時器通過運行循環啓動,而不是從某些NSTimer啓動方法啓動。)
  4. 提供模擬NSRunLoop並驗證addTimer:forMode:獲取調用。 (原因我不喜歡它:我不得不提供一個接口到運行循環?這似乎古怪。)

有人可以提供一些單元測試輔導嗎?

+0

您不應該驗證它是否真的被調度_運行。調度和運行位不是你的代碼;即使你單元測試了它們並且失敗了,你能做些什麼呢?你只需要確保你的代碼能夠完成它到目前爲止需要做的事情,並與框架正確接口。因此,我認爲第三是答案 - 告訴模擬'NSTimer'類對象期望'timerWithTimeInterval:...' –

+0

模擬'NSRunLoop'也是有意義的; Mike Ash有一篇文章可能會對此有所幫助:https://www.mikeash.com/pyblog/friday-qa-2010-01-01-nsrunloop-internals.html –

+0

Josh,感謝您的指導!我結束了對這個問題的更多思考,並且你對「調度和運行位不是你的代碼」的評論提醒我,我會在某處閱讀,「不要測試Apple的實現」。到現在爲止,它沒有任何意義。我試圖設計我的測試來覆蓋所有可能的場景,但是當你使用別人的代碼來做事情時你不能這麼做,你所能做的就是測試交互發生。 –

回答

6

好吧!花了一段時間才弄清楚如何做到這一點。我會全面解釋我的思維過程。抱歉,長期以來。我不得不弄清楚我測試的是什麼。我的代碼有兩件事:它啓動一個重複計時器,然後該計時器有一個回調讓我的代碼執行其他操作。這是兩個單獨的行爲,這意味着兩個不同的單元測試。

那麼如何編寫單元測試來驗證代碼是否正確啓動重複計時器?有三件事情就可以在一個單元測試測試:

  • 的方法
  • (優選通過一個公共接口提供)系統的狀態或行爲的改變
  • 的相互作用的返回值你的代碼有,你不控制

隨着NSTimerNSRunLoop一些其他的代碼,我必須測試的互動,因爲沒有辦法從外部驗證定時器配置正確。嚴重的是,沒有repeats財產。你必須攔截創建定時器本身的方法調用。

接下來,我意識到,如果我使用+scheduledTimerWithTimeInterval:target:selector:userInfo:repeats自動啓動計時器,我將不必觸碰NSRunLoop。這是我不得不測試的一個互動。

最後,爲了創建一個期望+scheduledTimerWithTimeInterval:target:selector:userInfo:repeats被調用,你必須模擬NSTimer類,幸好OCMock現在可以做到這一點。這裏的測試是什麼樣子:

id mockTimer = [OCMockObject mockForClass:[NSTimer class]]; 
[[mockTimer expect] scheduledTimerWithTimeInterval:1.0 
              target:[OCMArg any] 
              selector:[OCMArg anySelector] 
              userInfo:[OCMArg any] 
              repeats:YES]; 

<your code that should create/schedule NSTimer> 

[mockTimer verify]; 

看着這個測試,我想,「等一下,你怎麼可以實際測試計時器配置了正確的目標和選擇?」那麼,我終於意識到我不應該在乎它是否配置了一個特定的目標和選擇器,我應該只關心當計時器觸發時,它做我所需要的。這對於編寫好的,面向未來的單元測試來說非常重要:確實儘量不要依賴專用接口或實現細節,因爲這些事情會發生變化。相反,測試你的代碼不會改變的行爲,並通過公共接口來完成。

這將我們帶到了第二次單元測試:計時器是否做了我需要它做的事情?爲了測試這個,謝謝NSTimer-fire,這導致定時器在目標上執行選擇器。因此,你甚至都不需要創建一個假的NSTimer,或者做一個提取&覆蓋創建自定義的模擬定時器,所有你需要做的就是讓它撕裂:

id mockObserver = [OCMockObject observerMock]; 
[[NSNotificationCenter defaultCenter] addMockObserver:mockObserver 
               name:@"SomeNotificationName" 
               object:nil]; 
[[mockObserver expect] notificationWithName:@"SomeNotificationName" 
            object:[OCMArg any]]; 
[myCode startTimer]; 

[myCode.timer fire]; 

[mockObserver verify]; 
[[NSNotificationCenter defaultCenter] removeObserver:mockObserver]; 

有關此測試的幾點意見:

  • 當計時器火災,測試預計在一NSNotification發佈到默認NSNotificationCenterOCMock管理不讓人失望:通知廣播測試是這麼簡單。
  • 要實際觸發定時器觸發,您需要對定時器的引用。我的班級在測試中不公開NSTimer的公共接口,所以我這樣做的方式是創建一個類擴展,將私有的NSTimer屬性暴露給我的測試,as described in this SO post
+0

我是TDD新手,所以可能有些東西我可以忽略,但我不相信你的測試正在完成任何事情。當然,當你運行它時,你會得到一個綠色標記,但基本上你已經證實NSTimer實際上是按照標示的那樣工作。當你是蘋果公司的測試人員時,這很有用,但是它告訴你關於你的代碼的是什麼?據我所知,沒有什麼。在您的應用程序中,您可能會拼錯計時器的選擇器,指定錯誤的目標,在錯誤的時刻啓動計時器並忘記使計時器無效,但根據上述測試,您做得很好。 –

+0

測試的重點不在於驗證NSTimer是做什麼的 - 但是當它做它做的時候,你的被測代碼(CUT)做的是正確的事情。在一個* good *單元測試中,您想驗證它是否正確* public * thing - 改變一個公共變量,以一種公開的方式改變它的狀態,在第三方對象上調用一個方法,返回正確的價值,或者在這種情況下,廣播正確的通知。 –

+0

當然,您可能拼錯了計時器的選擇器,但是如果您這樣做了,它將不會廣播通知。或者您可以安排它每10秒運行一次,而不是每秒運行一次,但這就是爲什麼我設置模擬計時器以期望它能夠接收scheduledTimerWithTimeInterval:1.0。 最終,單元測試可以並不會提供100%的保證,你已經做了一切正確的事情。我的意思是,甚至期望scheduledTimerWithTimeInterval:是一個相當脆弱和脆弱的單元測試,因爲有人可以用另一個初始化器創建一個計時器。這就是集成/功能測試應該填補漏洞的地方 –

1

我真的很喜歡Richard的第一個方法,我擴大了代碼一點用塊調用,以避免引用私有財產NSTimer

[[mockAPIClient expect] someMethodSuccess:[OCMIsNotNilConstraint constraint] 
            failure:[OCMIsNotNilConstraint constraint]; 

id mockTimer = [OCMockObject mockForClass:[NSTimer class]]; 
[[[mockTimer expect] andDo:^(NSInvocation *invocation) { 
    SEL selector = nil; 
    [invocation getArgument:&selector atIndex:4]; 
    [testSubject performSelector:selector]; 
}] scheduledTimerWithTimeInterval:10.0 
          target:testSubject 
         selector:[OCMArg anySelector] 
         userInfo:nil 
          repeats:YES]; 

[testSubject viewWillAppear:YES]; 

[mockTimer verify]; 
[mockAPIClient verify]; 
+0

好!沒意識到你可以通過這種方式獲取發送給模擬對象的參數。不需要深入研究一個類的私有實現是一種改進。 我的確認爲它是以測試可讀性爲代價的,但像大多數情況一樣,這是一個折衷。 –