单元测试
假如我们今天去面试了,面试官问了一句“什么是单元测试?有没有使用?大概是针对那些情况进行单测的?单测意义从你实际使用中总结一下。”
这要在我没进行现在的单测之前这个问题我回答的可能就是“不好意思,我们公司项目没有使用单测,但我自己对单测还是有有点理解的,然后巴拉巴拉一顿操作.......”
那什么是单测?我们这期先说说关于业务代码的测试,后面我会在写一篇关于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; }
推荐的文章:
1、iOS单元测试初探以及OCMock使用入门
2、iOS单元测试[OCMock常见使用方式]
3、Kiwi 使用进阶 Mock, Stub, 参数捕获和异步测试
4、OCMock
5、OCMock Github