單元測試
假如我們今天去面試了,面試官問了一句“什么是單元測試?有沒有使用?大概是針對那些情況進行單測的?單測意義從你實際使用中總結一下。”
這要在我沒進行現在的單測之前這個問題我回答的可能就是“不好意思,我們公司項目沒有使用單測,但我自己對單測還是有有點理解的,然后巴拉巴拉一頓操作.......”
那什么是單測?我們這期先說說關於業務代碼的測試,后面我會在寫一篇關於UI測試的情況。
百度詞條給的解釋我就覺得前兩句話......

我就針對自己項目中的單測是實際例子總結一下自己對單元測試的一個理解。
什么情況下使用單測會比較好?
首先並不是所有的代碼或者業務都適合單元測試的,比如一段邏輯很簡單的代碼,你為了單測而單測那就真的沒有了意義。這是第一點就是邏輯很簡答的代碼是沒必要進行單測的。在實際使用中已經經過驗證的代碼是沒必要再走單測的,比如你寫了一個新的功能,然后用到了以前封裝的方法,這方法就沒必要再驗證一次。這里的意思是別做重復的工作!
新修改或者添加的業務邏輯比較復雜的代碼是適合單測的
比如下面我會和大家分享的一個業務模塊 - 退貨退款,這種業務模塊是很適合單測的,你想想退貨退款你需要涉及到的選擇退貨退款的商品的數據處理,這樣的數據處理單測很很好的幫你查找問題,再比如你寫了一堆涉及計算的代碼,算什么都行,但這塊代碼里涉及到大量的計算,這時候也是很適合走單測的。所以總體上來說需要梳理你自己處理數據並且邏輯相對復雜的就最好走單測。像我們熟知的AF、SD等等的單測覆蓋率是在60%-70%的樣子,已經很高了。
單元測試的模式
在單元測試的時候,不知道剛開始着手的時候你會不會想這樣一個問題,我該在什么時候進行單元測試?
可能有的人會想那必須等我把功能代碼全都寫完了才能針對這塊帶代碼進行單元測試呀,可能還有人會想那必須是我先寫單元測試呀,不然等我寫完代碼了發現這方法做單測還需要修改又給我增加工作量,方法還得寫兩遍,改的方法適合單測了要把我的業務代碼改的又需要我進行別的一大推的修改就不好了。其實這兩點考慮都都是需要相互補充的,那怎樣會適合呢?我們先給出下面兩點觀點:
1、Test Driven Development(TDD)
TDD模式:是先根據需求或者接口情況編寫測試,然后再根據測試來編寫業務代碼,這也就必然導致所有代碼的 public 部分都會需要必要的測試。
2、Behavior Driven Development(BDD)
BDD模式: 通過Given - When - Then三個流程化的條件來幫助開發確定應該測試什么。
我自己的理解:上面兩種模式了解之后我自己是這樣做的,最基本的業務代碼還是先開始寫,但你寫的時候一定要留意這地方是否需要單測,這點我相信你肯定能做好判斷,當你發現下面的業務代碼邏輯性會較強的時候,開始熊單測入手寫,這時候就轉到單元測試去寫,一邊單測一邊完善業務代碼,等你的單測都是通過的時候說明你這快的業務代碼其實是問題已經不大了的,這里進行單測的時候各種場景我們盡量想的全面一點對我們業務代碼完善是比較有好處的。等這業務代碼處理完的時候你就發現這點你的單測也完成的差不多了,能幫助你理解這塊業務的提示也能讓你及時的發現業務可能存在的問題,而不是因為產品或者我們都考慮的不全,等測試發現問題的時候我們再進行一個大手術。所以我自己在實際項目中也是這樣進行的,單測和業務同時進行。這是我自己的觀點,要是有別的想法的也可以提出來,我們一起探討一下。
XCTest + Mock
XCTest 在Xode里面這個就不再多說了,你在新建一個項目的時候會看到下面的選擇:

XCTest 我自己覺得要理解的第一點是各種各樣的斷言:
XCTFail(format…) 生成一個失敗的測試 XCTAssertNil(a, format...)為空判斷,a為空時通過,反之不通過 XCTAssertNotNil(a, format…)不為空判斷,a不為空時通過,反之不通過 XCTAssert(expression, format...)當expression求值為true時通過 XCTAssertTrue(expression, format...)當expression求值為true時通過 XCTAssertFalse(expression, format...)當expression求值為false時通過 XCTAssertEqualObjects(a, b, format...)判斷相等,[a isEqual:b]值為true時通過,其中一個不為空時,不通過 XCTAssertNotEqualObjects(a1, a2, format...)判斷不等,[a1 isEqual:a2]值為false時通過 XCTAssertEqual(a, b, format...)判斷相等(當a和b是 C語言標量、結構體或聯合體時使用,實際測試發現NSString也可以) XCTAssertNotEqual(a, b, format...)判斷不等(當a和b是 C語言標量、結構體或聯合體時使用) XCTAssertEqualWithAccuracy(a, b, accuracy, format...)判斷相等,(double或float類型)提供一個誤差范圍,當在誤差范圍(+/-accuracy)以內相等時通過測試 XCTAssertNotEqualWithAccuracy(a, b, accuracy, format...) 判斷不等,(double或float類型)提供一個誤差范圍,當在誤差范圍以內不等時通過測試 XCTAssertThrows(expression, format...)異常測試,當expression發生異常時通過;反之不通過;(很變態) XCTAssertThrowsSpecific(expression, specificException, format...) 異常測試,當expression發生specificException異常時通過;反之發生其他異常或不發生異常均不通過 XCTAssertThrowsSpecificNamed(expression, specificException, exception_name, format...)異常測試,當expression發生具體異常、具體異常名稱的異常時通過測試,反之不通過 XCTAssertNoThrow(expression, format…)異常測試,當expression沒有發生異常時通過測試 XCTAssertNoThrowSpecific(expression, specificException, format...)異常測試,當expression沒有發生具體異常、具體異常名稱的異常時通過測試,反之不通過 XCTAssertNoThrowSpecificNamed(expression, specificException, exception_name, format...)異常測試,當expression沒有發生具體異常、具體異常名稱的異常時通過測試,反之不通過
關於這個XC的斷言我就強調兩點:
1、根據判斷的條件選擇合適的斷言,並且要留意判斷的條件是在滿足怎樣的條件下是會進斷言的。
2、XCTest里面基本方法你要了解一下。
/// 單測開始 每一次你單測的方法點擊開始之后都會先走這個方法,所以你有需要初始化的東西可以寫在這里
- (void)setUp {
// Put setup code here. This method is called before the invocation of each test method in the class.
}
/// 單測結束
- (void)tearDown {
// Put teardown code here. This method is called after the invocation of each test method in the class.
}
- (void)testExample {
// This is an example of a functional test case.
// Use XCTAssert and related functions to verify your tests produce the correct results.
}
/** 測試性能 */
- (void)testPerformanceExample {
// This is an example of a performance test case.
[self measureBlock:^{
// Put the code you want to measure the time of here.
}];
}
Mock
說實話能進行mock的工具真的是太多了,我這里還是推薦一下 OCMock 吧。
至於iOS怎么引入OCMock大家直接去 官網文檔 查看就可以了,我們這里就不在多說了!當然你也可以使用CocoaPods直接 pod OCMock
下面這一大段代碼就是OCMock官網給出的它的基本的使用的中文版本,可以對比學習一下,不過在上手之前我還是建議大家讀一下這篇文章,能很好的幫你建立 stub 和 mock 的概念: Kiwi 使用進階 Mock, Stub, 參數捕獲和異步測試 這里是下面文章的官方地址
1.創建Mock對象
1.1 類Mock
id classMock = OCMClassMock([SomeClass class]);
1.2 協議Mock
id protocolMock = OCMProtocolMock(@protocol(SomeProtocol));
1.3 嚴格的類和協議Mock
默認的mock方式是nice(方法調用的時候返回nil或者是返回正確的方法)
嚴格的模式下,mock的對象在調用沒有被stub(置換)的方法的時候,會拋出異常.
id classMock = OCMStrictClassMock([SomeClass class]);id protocolMock = OCMStrictProtocolMock(@protocol(SomeProtocol));
1.4 部分Mock
id partialMock = OCMPartialMock(anObject)
這樣創建的對象在調用方法時:
如果方法被stub,調用stub后的方法.
如果方法沒有被stub,調用原來的對象的方法.
partialMock 對象在調用方法后,可以用於稍后的驗證此方法的調用情況(被調用,調用結果)
1.5 觀察者Mock
id observerMock = OCMObserverMock();
這樣創建的對象可以用於觀察/通知.
2 置換方法
2.1 置換方法(待置換的方法返回objects)
OCMStub([mock someMethod]).andReturn(anObject);
在mock對象上調用某個方法的時候,這個方法一定返回一個anObject.(也就是說強制替換了某個方法的返回值為anObject)
2.2 置換方法(待置換的方法返回values)
OCMStub([mock aMethodReturningABoolean]).andReturn(YES);
在mock對象上調用某個方法的時候,這個方法一定返回values.
注意這里的原始值類型一定要和原來的方法的返回值一致.
2.3 委托到另一個方法(置換委托方法到另外一個方法)
OCMStub([mock someMethod]).andCall(anotherObject, @selector(aDifferentMethod));
置換mock 對象的someMethod ==> anotherObject 的aDifferentMethod.
這樣,當mock對象調用someMethod方法的時候,實際上的操作就是anotherObject 調用了aDifferentMethod方法.
2.4 置換一個blcok方法.
OCMStub([mock someMethod]).andDo(^(NSInvocation invocation) { / block that handles the method invocation */ });
在mock對象調用someMethod的時候,andDo后面的block會調用.block可以從NSInvocation中得到一些參數,然后使用這個NSInvocation對象來構造返回值等等.
2.5 置換方法的參數
OCMStub([mock someMethodWithReferenceArgument:[OCMArg setTo:anObject]]);
OCMStub([mock someMethodWithReferenceArgument:[OCMArg setToValue:OCMOCK_VALUE((int){aValue})]]);
mock對象在調用某個帶參數的方法的時候,這個方法的參數可以被置換.
setTo用來設置對象參數,setToValue用來設置原始值類型的參數.
2.6 調用某個方法就拋出異常
OCMStub([mock someMethod]).andThrow(anException);
當mock對象調用someMethod的時候,就會拋出異常
2.7 調用某個方法就發送通知
OCMStub([mock someMethod]).andPost(aNotification);
當mock對象調用someMethod的時候,就會發送通知.
2.8 鏈式調用
OCMStub([mock someMethod]).andPost(aNotification).andReturn(aValue);
所有的actions(比如andReturn,andPost)可以鏈式調用.上面的例子中,mock對象調用someMethod方法后,發送通知,返回aValue
2.9 轉發的原來的對象/類
OCMStub([mock someMethod]).andForwardToRealObject();
使用部分mock的時候,使用類方法的可以轉發到原來的對象/原來的類.
這個功能在鏈式調用或者是使用expectation的時候很有用.
2.10 什么也不做
OCMStub([mock someMethod]).andDo(nil);
可以給andDo傳入nil參數,而不是原來一個block作為參數.
這個功能在使用部分mock/mock類的時候很有用,可以屏蔽原來的行為.
3 驗證作用
3.1 運行后就驗證
id mock = OCMClassMock([SomeClass class]);
/* run code under test */
OCMVerify([mock someMethod]);
在mock對象調用someMethod后就開始驗證.(如果這個方法沒有被調用),就拋出一個錯誤.
在驗證語句中可以使用 參數約束.
3.2 置換后驗證
id mock = OCMClassMock([SomeClass class]);
OCMStub([mock someMethod]).andReturn(myValue);
/* run code under test */
OCMVerify([mock someMethod]);
在置換某個方法(置換了返回的參數)后,然后可以驗證這個方法是否被調用.
4 參數約束
4.1 任意參數約束
OCMStub([mock someMethodWithAnArgument:[OCMArg any]])
OCMStub([mock someMethodWithPointerArgument:[OCMArg anyPointer]])
OCMStub([mock someMethodWithSelectorArgument:[OCMArg anySelector]])
不管傳遞什么參數,對於所有活躍的invocations,置換該方法.Pointers 和selectors 需要像上面一樣特殊對待.對於既不是對象,也不是指針,更不是SEL類型的,不可以忽略的參數,可以使用 any 來代替.
4.2 忽略非對象的參數
[[[mock stub] ignoringNonObjectArgs] someMethodWithIntArgument:0]
在這個invocation中,mock忽略所有的非對象參數.mock對象將會接收所有的someMethodWithIntArgument 方法 invocation,而不去管實際傳遞進來的參數是什么.如果這個方法含有對象參數和非對象參數,對象參數仍然可以使用OCMArg的參數約束.
4.3 匹配參數
OCMStub([mock someMethod:aValue)
OCMStub([mock someMethod:[OCMArg isNil]])
OCMStub([mock someMethod:[OCMArg isNotNil]])
OCMStub([mock someMethod:[OCMArg isNotEqual:aValue]])
OCMStub([mock someMethod:[OCMArg isKindOfClass:[SomeClass class]]])
OCMStub([mock someMethod:[OCMArg checkWithSelector:aSelector onObject:anObject]])
OCMStub([mock someMethod:[OCMArg checkWithBlock:^BOOL(id value) { /* return YES if value is ok */ }]])
如果在置換創建的時候,有個一個參數傳遞進來了,置換方法將僅僅匹配精確參數的invocations.帶不同的參數來調用的方法不會被匹配.
OCMArg類提供了幾個不同的方法來匹配不同的參數類型.
對於checkWithSelector:onObject:方法, 當mock對象接收到someMethod:的時候, 會觸發 anObject上的aSelector方法. 如果方法帶參數,這個參數會傳遞給someMethod:. 這個方法應該返回一個BOOL值,表示這個參數是否和預期的一樣.
4.4 使用Hamcrest來匹配
OCMStub([mock someMethod:startsWith(@"foo")]
OCMock不帶 Hamcrest 框架,所以如果想要使用的話,需要自己安裝Hamcrest .
5 類方法的Mock
5.1 置換類方法
id classMock = OCMClassMock([SomeClass class]);
OCMStub([classMock aClassMethod]).andReturn(@"Test string");
// result is @"Test string"
NSString *result = [SomeClass aClassMethod];
置換類方法和置換實例方法的步驟相像.但是mock對象在深層次上對原有 類做了些更改.(替換了原有的的類的meta class).這讓置換調用直接作用在mock對象上,而不是原有的類.
注意:
添加到類方法上的mock對象跨越了多個測試,mock的類對象在置換后不會deallocated,需要手動來取消這個mock關系.
如果mock對象作用於同一個類, 這時的行為就不預測了.
5.2 驗證類方法的調用
id classMock = OCMClassMock([SomeClass class]);
/* run code under test */
OCMVerify([classMock aClassMethod]);
驗證類方法的調用和驗證實例方法的調用的使用方式一樣.
5.3 有歧義的類方法和實例方法
id classMock = OCMClassMock([SomeClass class]);
OCMStub(ClassMethod([classMock ambiguousMethod])).andReturn(@"Test string");
// result is @"Test string"
NSString *result = [SomeClass ambiguousMethod];
置換了類方法,但是類有一個和類方法同名的實例方法,置換類方法的時候,必須使用ClassMethod()
5.4 恢復類
id classMock = OCMClassMock([SomeClass class]);
/* do stuff */
[classMock stopMocking];
置換類方法后,可以將類恢復到原來的狀態,通過調用stopMocking來完成.
如果在結束測試前,需要恢復到原來的狀態的話,這就很有用了.
在mock對象被釋放的時候,stopMocking會自動調用.
當類恢復到原來的對象,類對象的meta class會變為原來的meta class.這會移除所有的方法置換.
在調用了stopMocking之后,不應該繼續使用mock對象.
6 部分Mock
6.1 置換方法
id partialMock = OCMPartialMock(anObject);
OCMStub([partialMock someMethod]).andReturn(@"Test string");
// result1 is @"Test string"
NSString *result1 = [partialMock someMethod];
// result2 is @"Test string", too!
NSString *result2 = [anObject someMethod];
部分Mock修改了原有的mock對象的類.(實際上是繼承了待mock對象,然后替換用 繼承的類來代替原有的類).
這就是說: 使用真實的對象來調用,即使是使用self,也會影響 置換方法和預期的結果.
6.2 驗證方法調用
id partialMock = OCMPartialMock(anObject);
/* run code under test */
OCMVerify([partialMock someMethod]);
驗證方法的調用和驗證類方法,驗證協議的調用類似.
6.3 恢復對象
id partialMock = OCMPartialMock(anObject);
/* do stuff */
[partialMock stopMocking];
真正的對象可以通過調用stopMocking方法來恢復到原來的狀態.
這種情況只有在結束測試之前需要恢復到原來狀態.
部分mock對象會在釋放的時候,會自動調用 stopMocking方法.
當對象轉變為原來的狀態后,類會變為原來的類.也會移除所有的置換方法.
在調用了stopMocking之后,最好不要去使用mock對象.
7 嚴格mock和期望
7.1 Expect-run-verify 期望-運行-驗證
id classMock = OCMClassMock([SomeClass class]);
OCMExpect([classMock someMethodWithArgument:[OCMArg isNotNil]]);
/* run code under test, which is assumed to call someMethod */
OCMVerifyAll(classMock)
這是使用mock最原始的方法:
創建mock對象
期望調用某個方法
測試代碼(預想的是這段測試代碼會調用上面期望調用的方法.
驗證mock對象(也就是驗證期望的方法是否被調用了)
如果預期的方法沒有被調用,或者調用的時候,傳遞的參數不對,那么就好產生錯誤.可以使用上面 參數約束.
嚴格的mock可以用在類和協議上.
如果有懷疑的話,可以使用 3 驗證作用
7.2 嚴格的mock 和快速失敗
id classMock = OCMStrictClassMock([SomeClass class]);
[classMock someMethod]; // this will throw an exception
上面mock沒有設置任何期望,直接掉調用某個方法會拋出異常.
當超出去預期的調用的時候,會立即測試失敗. 只有strict mock才會快速失敗.
7.3 置換操作和預期
id classMock = OCMStrictClassMock([SomeClass class]);
OCMExpect([classMock someMethod]).andReturn(@"a string for testing");
/* run code under test, which is assumed to call someMethod */
OCMVerifyAll(classMock)
可以使用andReturn,andThrow,等預期的操作.如果方法被調用,會調用置換 方法,確認方法確實被調用了.
7.4 延時驗證
id mock = OCMStrictClassMock([SomeClass class]);
OCMExpect([mock someMethod]);
/* run code under test, which is assumed to call someMethod eventually */
OCMVerifyAllWithDelay(mock, aDelay);
在某種情況下,預期的方法只有在 run loop 出於活躍狀態的時候才會被調用.這時,可以將認證延時一會.aDelay是mock對象會等待的最大時間.通常情況下,在預期達到后就會返回.
7.5 依序驗證
id mock = OCMStrictClassMock([SomeClass class]);
[mock setExpectationOrderMatters:YES];
OCMExpect([mock someMethod]);
OCMExpect([mock anotherMethod]);
// calling anotherMethod before someMethod will cause an exception to be thrown
[mock anotherMethod];
mock會按照在預期中設置好的順序來判斷.只要調用的不是按照期望的調用順序,這個時候就會拋出異常.
8 觀察者mock
8.1 准備工作
id observerMock = OCMObserverMock();
[notificatonCenter addMockObserver:aMock name:SomeNotification object:nil];
[[mock expect] notificationWithName:SomeNotification object:[OCMArg any]];
為觀察者和通知創建一個mock對象.
在通知中心注冊對象
預期會調用這個通知.
8.2 驗證
OCMVerifyAll(observerMock);
目前觀察者 mock 總是嚴格的mock.當一個不在預期中的通知調用的時候,就會拋出一個異常.
這就是說,單個的通知實際上不是能被驗證的.所有的通知必須按照預期賴設置.他們會在通過調用OCMVerifyAll來一起驗證.
9 進階話題
9.1 對於普通的mock,快速失敗
對strict mock 對象,在一個mock對象上調用沒有被mock方法(沒有被置換)的時候,會拋出一個異常,這時候會發生 快速失敗.
id mock = OCMClassMock([SomeClass class]);
[[mock reject] someMethod];
這種情況下,mock會接受除了someMethod 的所有方法.觸發someMethod方法會導致快速失敗.
9.2 在OCMVerifyAll時重新拋出異常
在fail-fast的時候會拋出異常,但是這並不一定會導致測試失敗.
通過調用OCMVerifyAll重新拋出異常可以導致測試失敗.
這個功能在不在預期中的從notifications引發的invocations出現的時候使用.
9.3 置換創建對象的方法
id classMock = OCMClassMock([SomeClass class]);
OCMStub([classMock copy])).andReturn(myObject);
可以置換創建對象的 類/實例方法.當被置換的方法以 alloc,new,copy,mutableCopy開頭的方法時,OCMock會自動調整對象的引用計數.
id classMock = OCMClassMock([SomeClass class]);
OCMStub([classMock new])).andReturn(myObject);
盡管可以置換類的new方法,但是不建議這么做.
沒有辦法置換 init 方法,因為這個方法是被mock對象自己實現的.
9.4 基於實例對象的方法替換
id partialMock = OCMPartialMock(anObject);
OCMStub([partialMock someMethod]).andCall(differentObject, @selector(differentMethod));
用一句話概括起來,Method swizzling 會在運行時替換一個方法的實現.
使用 partial mock然后調用 andCall操作可以實現這個方法替換.
當anObject收到someMethod消息時,anObject的實現沒有觸發,相反的,
differentObject的differentMethod得到調用.
其他方法並不會收到影響,仍然會調用原來的的方法的實現.
10 使用限制
10.1 在一個指定的類上,只能有一個mock對象
// don't do this
id mock1 = OCMClassMock([SomeClass class]);
OCMStub([mock1 aClassMethod]);
id mock2 = OCMClassMock([SomeClass class]);
OCMStub([mock2 anotherClassMethod]);
原因是類的meta class 替換后,不會釋放,mock類仍會存在,甚至可能跨tests.
如果多個相同mock對象管理同一個類,運行時的行為就不可確定.
10.2 在被置換的方法上設置期望,會不起作用
id mock = OCMStrictClassMock([SomeClass class]);
OCMStub([mock someMethod]).andReturn(@"a string");
OCMExpect([mock someMethod]);
/* run code under test */
OCMVerifyAll(mock); // will complain that someMethod has not been called
上面代碼先替換了someMethod,然后強制someMethod返回”a string"
由於現在mock的實現,所有的someMethod都會置換所處理.所以,即使這個方法被調用,這個驗證也會失敗.
可以通過在expect后添加andReturn來避免這個問題. 也可以通過在expect后再次設置一個方法替換.
10.3 Partial mock 不能在某些特定的類使用
id partialMockForString = OCMPartialMock(@"Foo");
// will throw an exception
NSDate *date = [NSDate dateWithTimeIntervalSince1970:0];
id partialMockForDate = OCMPartialMock(date);
// will throw on some architectures
不可能創建一個 toll-free bridged的類,例如 NSString,或者是NSDate.
如果你試圖這么去做,那么可能會拋出一個異常.
10.4 某些特定的類不能被置換和驗證
id partialMockForString = OCMPartialMock(anObject);
OCMStub([partialMock class]).andReturn(someOtherClass); // will not work
不能mock某些運行時的方法,例如
class,
methodSignatureForSelector:
forwardInvocation:
10.5 NSString的類方法不能被置換和驗證
id stringMock = OCMClassMock([NSString class]);
// the following will not work
OCMStub([stringMock stringWithContentsOfFile:[OCMArg any] encoding:NSUTF8StringEncoding error:[OCMArg setTo:nil]]);
10.6 NSObject 的方法不能被驗證
id mock = OCMClassMock([NSObject class]);
/* run code under test, which calls awakeAfterUsingCoder: */
OCMVerify([mock awakeAfterUsingCoder:[OCMArg any]]);
// still fails
不可能在NSObject 和它的分類category上使用verify-after-running.
在某些情況下可能置換這個方法,然后驗證.
10.7 apple 的私有方法不能被驗證
UIWindow window = / get window somehow /
id mock = OCMPartialMock(window);
/ run code under test, which causes _sendTouchesForEvent: to be invoked */
OCMVerify([mock _sendTouchesForEvent:[OCMArg any]]);
// still fails
含有下划線前綴,后綴,NS,UI開頭的方法等.
10.8 Verify-after-running不能使用延時
只有在 嚴格的mock和期望中,可以使用expect-run-verify
簡單的小Demo
1、下面這段代碼你不用理解,你只要知道這是我本地處理的一個計算退款金額的方法:
/// 計算退款的金額
-(void)setRefundAmount{
/// 全單退
if (self.refundSyncApplyType == allApplyType) {
self.refundAmount = self.totalPrice;
/// 部分退
}else{
CGFloat refundPrice = 0;
for (RefundSyncGoodsModel * goodsModel in self.refundChooseGoods) {
for (RefundSyncGoodsAttrModel * attrModel in goodsModel.attr) {
refundPrice += (attrModel.selectNum.intValue * goodsModel.payPrice.floatValue);
}
}
self.refundAmount = [NSString stringWithFormat:@"%.2f",refundPrice];
}
/// 大於0的情況
if (self.refundAmount.floatValue > 0) {
if (self.refershRefundAmount) {
self.refershRefundAmount(self.refundAmount);
}
/// 不大於0的情況 不設置金額
}else{
if (self.refershRefundAmount) {
self.refershRefundAmount(@"");
}
}
}
但這樣一個方法你就得驗證一下他到底對還是不對了,這里我們就得走單測看看:
#pragma mark -- set amount
/// 單測全單退款計算商品退款金額
-(void)testRefundAllGoodsWithRefundAmount{
/// 全單退貨
[self.applyViewModel.refundGoodsArray addObject: [self defaultGoodsData]];
[self.applyViewModel.refundChooseGoods addObject: [self defaultGoodsData]];
self.applyViewModel.refundSyncApplyType = allApplyType;
__block NSMutableString * oderAmount = [NSMutableString string];
self.applyViewModel.refershRefundAmount = ^(NSString * _Nonnull amount) {
NSLog(@"同步退款-全單退貨顯示訂單總價:%@",amount);
[oderAmount appendString:amount];
/// 為什么下面寫法報循環引用的錯誤
/// XCTAssertEqualObjects(amount,@"900.00",@"同步退款-退款全單計算退款金額有誤");
};
/// 測試計算退款金額的方法
[self.applyViewModel setRefundAmount];
/// 假設退款一件 全單總價1000 全單退款金額顯示的是剩余的訂單總價 - 就應該是900
XCTAssertEqualObjects(oderAmount,@"900.00",@"同步退款-退款全單計算退款金額有誤");
}
2、還有一個異步測試的小例子,也可以拿出來看看:
/// 單測請求退款原因數據
-(void)testRequestRefundReasonData{
XCTestExpectation * excpection = [self expectationWithDescription:@"同步退款-測試請求退款原因數據"];
[self.applyViewModel requestRefundReasonDataSuccess:^(NSDictionary * _Nonnull respond) {
NSLog(@"同步退款-原因數據:%@",respond);
/// 測試handleRefundReasonListData處理數據方法
/// 條件為true的時候通過 否則打印錯誤日志
XCTAssertTrue(self.applyViewModel.refundReasonArray.count != 0,@"同步退款-退款原因數據處理出錯,列表為空!");
/// 達到預期效果
[excpection fulfill];
} andFailure:^(PPReqeustError * _Nonnull error) {
XCTFail(@"同步退款-原因數據請求出錯:%@",error.message);
}];
/*
#define PP_WAIT do {\
[self expectationForNotification:@"PPBaseTest" object:nil handler:nil];\
[self waitForExpectationsWithTimeout:15 handler:nil];\
} while (0);
*/
PP_WAIT;
}
推薦的文章:
3、Kiwi 使用進階 Mock, Stub, 參數捕獲和異步測試
4、OCMock
