Detailed Explanation of OCMock, a Common Framework for iOS Unit Testing

0 22
I. Unit Testing1.1 Necessity of Unit TestingTest-driven development is not a new...

I. Unit Testing

1.1 Necessity of Unit Testing

Test-driven development is not a new concept. In daily development, there is often a need for testing, but this output is something that must be displayed on the screen after clicking a series of buttons. When testing, it is often necessary to start the app from scratch using an emulator, locate the program of the module you are in, perform a series of click operations, and then check if the results are in line with your expectations.

Detailed Explanation of OCMock, a Common Framework for iOS Unit Testing

This behavior is undoubtedly a huge waste of time. Therefore, many senior engineers have found that we can construct a similar scenario in the code, then call the code we want to check before, and compare the running results with the expected results in the program. If they are consistent, it means that our code has no problems, and thus unit testing was born.

1.2 Purpose of Unit Testing

The main purpose of unit testing is to discover logical, syntactic, algorithmic, and functional errors within the module.

Unit testing is mainly based on white-box testing to verify the following issues:

  • Verify the consistency between the code and the design.
  • Discover errors existing in design and requirements.
  • Discover errors introduced during coding.

The focus of unit testing is as follows:

1686885069_648bd2cdbb058d55764e0.png!small?1686885070414

Independent path - For basic execution paths and loops, possible errors include:

  • Comparison of different data types.
  • “One less error”, that is, it may be one more or one less loop.
  • Error or impossible termination conditions.
  • Improper modification of loop variables.

Local data structure - The local data structure of the unit is the most common source of errors, and test cases should be designed to check for possible errors:

  • Inconsistent data types.
  • Check incorrect or inconsistent data types.

Error handling - A well-designed unit requires the ability to anticipate error conditions and set appropriate error handling to ensure that errors can be rearranged logically when the program fails, ensuring the correctness of the logic:

  • The description of the error is difficult to understand.
  • The displayed error does not match the actual error.
  • Improper handling of error conditions.

Boundary conditions - Errors at the boundary are the most common error phenomena:

  • Errors occur when taking the maximum and minimum values.
  • Comparison values such as greater than and less than in the control flow often occur errors.

Unit interface - The interface is actually a collection of input and output corresponding relationships. To perform dynamic testing on the unit, you simply need to provide an input to this unit and then check whether the output is consistent with the expected result. If the data cannot be input and output normally, unit testing is impossible. Therefore, the following tests need to be performed on the unit interface:

  • Whether the input and output of the unit under test are consistent in terms of number, properties, and sequence with the description in the detailed design.
  • Whether the input-only formal parameters have been modified.
  • Whether the constraint conditions are transmitted through formal parameters.

1.3 The two main frameworks for unit test dependencies

OCUnit (that is, testing with XCTest) is actually Apple's built-in testing framework, mainly for assertion usage. Due to its simplicity, this article will not go into detail.

The main function of OCMock is to simulate the return value of a method or property. You may wonder why we need to do this? Isn't it okay to use the model-generated model object and pass it in? The answer is yes, but there are special cases, such as objects that are not easy to construct or obtain. In such cases, you can create a virtual object to complete the test. The implementation idea is to create an object corresponding to the class of the object to be mocked, and set up the properties of the object and the actions after calling the predefined methods (such as returning a value, executing a code block, sending messages, etc.), and then record it in an array. Next, the developer actively calls the method, and finally performs a verify (verification) to determine whether the method has been called, or whether an exception was thrown during the call. In unit test development, the use of more difficult points is also due to the unclear use of OCMock. This article mainly discusses the integration and usage methods of OCMock.

Second, the integration and usage of OCMock

2.1 OCMock integration methods

Integrate the third-party library OCMock into the project, you can directly install the OCMock framework using the pod tool. If you need to install the OCMock library using the iBiu tool, you need to create a Podfile.custom file at the same level as the podfile file.

1686885116_648bd2fc2a475d3113ddf.png!small?1686885117693

Add OCmock in the same format as a regular pod file as follows:

source 'https://github.com/CocoaPods/Specs.git'
pod 'OCMock'

2.2 OCMock usage methods

(1) Method substitution (stub): Tell the mock object what value to return when someMethod is called

Calling method:

d jalopy = [OCMock mockForClass[Car class]];
OCMStub([jalopy goFaster:[OCMArg any] units:@"kph"]).andReturn(@"75kph");

Use case:

1. When verifying Method A, if Method A uses the return value of Method B, but Method B has complex internal logic, it is necessary to use the stub method to stub the return value of Method B. The code implementation is similar to the following code to fix the return value of funcB, achieving the goal of obtaining the parameters needed for testing without affecting the source code.

Before stubbing the method

- (NSString *)getOtherTimeStrWithString:(NSString *)formatTime{
    NSDateFormatter *formatter = [[NSDateFormatter alloc] init];
    [formatter setDateStyle:NSDateFormatterMediumStyle];
    [formatter setTimeStyle:NSDateFormatterShortStyle];
    [formatter setDateFormat:@"YYYY-MM-dd HH:mm:ss"]; //(@"YYYY-MM-dd hh:mm:ss") ----------Set the desired format, the difference between hh and HH: they represent 12-hour and 24-hour formats respectively
    //Set the time zone to Beijing time zone
    NSTimeZone* timeZone = [NSTimeZone timeZoneWithName:@"Asia/Beijing"];
    [formatter setTimeZone:timeZone];
    NSDate* date = [formatter dateFromString:formatTime]; //------------Convert string to NSDate using formatter
    //Method to convert time to timestamp:
    NSInteger timeSp = [[NSNumber numberWithDouble:[date timeIntervalSince1970]] integerValue] * 1000;
    return [NSString stringWithFormat:@"%ld",(long)timeSp];
}

Using stub (mockObject getOtherTimeStrWithString).andReturn (@"1000") will have an effect similar to the following before stubbing the method

- (NSString *)getOtherTimeStrWithString:(NSString *)formatTime{
    
    return @"1000";
    
    NSDateFormatter *formatter = [[NSDateFormatter alloc] init];
    [formatter setDateStyle:NSDateFormatterMediumStyle];
    [formatter setTimeStyle:NSDateFormatterShortStyle];
    [formatter setDateFormat:@"YYYY-MM-dd HH:mm:ss"]; //(@"YYYY-MM-dd hh:mm:ss") ----------Set the desired format, the difference between hh and HH: they represent 12-hour and 24-hour formats respectively
    //Set the time zone to Beijing time zone
    NSTimeZone* timeZone = [NSTimeZone timeZoneWithName:@"Asia/Beijing"];
    [formatter setTimeZone:timeZone];
    NSDate* date = [formatter dateFromString:formatTime]; //------------Convert string to NSDate using formatter
    //Method to convert time to timestamp:
    NSInteger timeSp = [[NSNumber numberWithDouble:[date timeIntervalSince1970]] integerValue] * 1000;
    return [NSString stringWithFormat:@"%ld",(long)timeSp];
}

2. The normal code flow has been tested and is very robust, but some error flows are not easy to find but may exist, such as edge value data. In unit tests, stubs can be used to simulate data, and the running status of the test code under special data conditions can be tested.

Note: The stub() can also be set without a return value, which is feasible, and it is guessed that the return value may be nil or void, so methods without return values can also be stubbed.

(II) There are currently three ways to generate Mock objects.

This example tests the talk method of the Person class, which also involves the Men class and the Animation class. The following is the source code of the three classes.

Person class

@interface Person()
@property(nonatomic,strong)Men *men;
@end


@implementation Person
-(void)talk:(NSString *)str
{
    [self.men logstr:str];
    [Animation logstr:str];
    
}
@end

Men class

@implementation Men
-(NSString *)logstr:(NSString *)str
{
    NSLog(@"%@",str);
    return str;
}
@end

Animation class

@implementation Animation
+(NSString *)logstr:(NSString *)str
{
    NSLog(@"%@",str);
    return str;
}
-(NSString *)logstr:(NSString *)str
{
    NSLog(@"%@",str);
    return str;
}
@end

When performing unit testing on the talk method, a mock of the person class is required. The following describes three different ways to generate mock objects, introduces the calling methods and use cases of the three methods, and finally presents a table of the advantages and disadvantages of each method for easy distinction.

Nice Mock

The mock objects created by NiceMock will prefer to call instance methods during method testing, and if the instance method is not found, it will continue to call the同名 class method. Therefore, this method can be used to generate mock objects to test class methods as well as object methods.

使用方式:

- (void)testTalkNiceMock {
    id mockA = OCMClassMock([Men class]);
    Person *person1 = [Person new];
    person1.men = mockA;
    [person1 talk:@"123"];
    OCMVerify([mockA logstr:[OCMArg any]]);
}

Use case:

Nice mock is relatively friendly, when an unstubbed method is called, it will not cause an exception and will pass the verification. If you don't want to stub many methods yourself, then use nice mock. In the above example, when mockA calls testTalkNiceMock, the + (NSString *)logstr:(NSString *)str method in the Men class will not perform the print operation. During the call, because there are both class methods and instance methods with the same name, the instance method will be called first.

Strict Mock

使用方式:

The test case is as follows, if mockA is generated to call the testTalkStrictMock method, then the method generated by the Mock to call testTalkStrictMock must use stubbing, otherwise the final OCMVerifyAll(mockA) will throw an exception.

- (void)testTalkStrictMock {
    id mockA = OCMStrictClassMock([Person class]);
    OCMStub([mockA talk:@"123"]);
    [mockA talk:@"123"];
    OCMVerifyAll(mockA);
}

Use case:

The mock objects created in this way will throw an exception if an unstubbed (stub represents a stub) method is called. This requires ensuring that every independent call to a method within the mock's lifecycle is stubbed, which is a method that is used relatively strictly and rarely.

Partial Mock

这样创建的对象在调用方法时:如果方法被 stub, 调用 stub 后的方法,如果方法没有被 stub, 调用原来的对象的方法,该方法有限制只能 mock 实例对象。

使用方式:

- (void)testTalkPartialMock {
    id mockA = OCMPartialMock([Men new]);
    Person *person1 = [Person new];
    person1.men = mockA;
    [person1 talk:@"123"];
    OCMVerify([mockA logstr:[OCMArg any]]);
}

Use case:

当调用一个没有被存根的方法时,会调用实际对象的该方法。当不能很好的存根一个类的方法时,该技术是非常有用的。调用 testTalkPartialMock 时 Men 类中的 +(NSString *) logstr:(NSString *) str 会执行打印操作。

三种方式的差异表格:

1686885125_648bd3054310d9a36d511.png!small?1686885125889

(三)验证方法的调用

Calling method:

OCMVerify([mock someMethod]);
OCMVerify(never(),    [mock doStuff]); //从没被调用
OCMVerify(times(n),   [mock doStuff]);   //调用了N次
OCMVerify(atLeast(n), [mock doStuff]);  //最少被调用了N次
OCMVerify(atMost(n),  [mock doStuff]);

Use case:

在单元测试中可以验证某个方法是否执行,以及执行了几次。

延时验证调用:

OCMVerifyAllWithDelay(mock, aDelay);

使用场景:该功能用于等待异步操作会比较多,其中 aDelay 为预期最长等待时间。

(四)添加预期

Calling method:

NSDictionary *info = @{@
id mock = OCMClassMock([MOOCMockDemo class]);

Add expectations:

OCMExpect([mock handleLoadSuccessWithPerson:[OCMArg any]]);

Non-execution can be expected:

OCMReject([mock handleLoadFailWithPerson:[OCMArg any]]);

Parameters can be verified:

// Expected + Parameter Verification
OCMExpect([mock handleLoadSuccessWithPerson:[OCMArg checkWithBlock:^BOOL(id obj) {
    MOPerson *person = (MOPerson *)obj;
    return [person.name isEqualToString:@"momo"];
}

Execution order can be expected:

// Expected to execute the following methods in order
[mock setExpectationOrderMatters:YES];
OCMExpect([mock handleLoadSuccessWithPerson:[OCMArg any]]);
OCMExpect([mock showError:NO]);

Parameters can be ignored (expected method execution):

OCMExpect([mock showError:YES]).ignoringNonObjectArgs; // Ignore parameters

Execution:

[MOOCMockDemo handleLoadFinished:info];

Assertion:

OCMVerifyAll(mock);

Assertion can be delayed:

OCMVerifyAllWithDelay(mock, 1); // Supports delayed verification

The final OCMVerifyAll will verify if the expectations before it are valid. If any of them are not called, an error will occur.

(V) Parameter Constraints

Calling method:

OCMStub([mock someMethodWithAnArgument:[OCMArg any]])
OCMStub([mock someMethodWithPointerArgument:[OCMArg anyPointer]])
OCMStub([mock someMethodWithSelectorArgument:[OCMArg anySelector]])

Use case: When using the OCMVerify() method to verify whether a method is called, the unit test will verify if the method parameters are consistent. If they are not consistent, it will prompt verification failure. At this point, if you only focus on method calls and not on parameters, you can use [OCMArg any] to pass parameters.

(6) Simulation of network interfaces

As the name implies, it can mock the data return of network interfaces, test the direction and accuracy of the code under different data.

Calling method:

id mockManager = OCMClassMock([JDStoreNetwork class]);
[orderListVc setComponentsNet:mockManager];
[OCMStub([mockManager startWithSetup:[OCMArg any] didFinish:[OCMArg any] didCancel:[OCMArg any]]) andDo:^(NSInvocation *invocation) {   


    void (^successBlock)(id components,NSError *error) = nil;   
    
    [invocation getArgument:&successBlock atIndex:3];  
    
    successBlock(@{"code":"1","resultCode":"1","value":{"showOrderSearch":"NO"}},nil);
    };

The above is to call the interface within the setComponentsNet method, and this method can simulate the required return data after calling the interface, and the successBlock contains the returned test data. This method is to manually call by obtaining the method signature of the interface call, obtaining the successBlock successful callback parameters, and manually calling. It can also simulate the failure of the interface, just need to obtain the corresponding failure callback in the signature to achieve this.

Use case: When writing unit test methods involving network interface simulation, use this method to mock the interface return results.

(7) Restore the class

After replacing class methods, the class can be restored to its original state by calling stopMocking.

Calling method:

id classMock = OCMClassMock([SomeClass class]);
/* do stuff */
[classMock stopMocking];

Use case:

After normal replacement of instance objects, the mock object will automatically call stopMocking after release, but the mock object added to the class method will span multiple tests, and the class object replaced will not be deallocated. It is necessary to manually cancel this mock relationship.

(Eight) Observer simulation - Create an instance that accepts notifications

Calling method:

- (void)testPostNotification {   
Person *person1 = [[Person alloc] init];   
id observerMock = OCMObserverMock();   
// Set observer for notification center    
[[NSNotificationCenter defaultCenter] addMockObserver: observerMock name:@"name" object:nil];    
// Set observation expectation    
[[observerMock expect] notificationWithName:@"name" object:[OCMArg any]];    // Call the method to be verified    
[person1 methodWithPostNotification];    
[[NSNotificationCenter defaultCenter] removeObserver:observerMock];    
// Call verification   
OCMVerifyAll(observerMock);}

Use case:

Create a mock object that can be used to observe notifications. The mock must be registered to receive notifications.

(Nine) mock protocol

Calling method:

id protocolMock = OCMProtocolMock(@protocol(SomeProtocol));
/*Strict protocol*/
id classMock = OCMStrictClassMock([SomeClass class]);
id protocolMock = OCMStrictProtocolMock(@protocol(SomeProtocol));
id protocolMock = OCMProtocolMock(@protocol(SomeProtocol));
/*Strict protocol*/
id classMock = OCMStrictClassMock([SomeClass class]);
id protocolMock = OCMStrictProtocolMock(@protocol(SomeProtocol));

Use case: Use it when you need to create an instance with the defined functionality of the protocol.

2.3 Limitations of mock

It is not allowed to stub first and then expect for the same method: because if stubbing is done first, all calls will become stubs, and even if the process calls this method, the final OCMVerifyAll verification will fail; the solution is to stub while using OCMExpect, for example: OCMExpect ([mock someMethod]).andReturn (@"a string"), or place the stub after the expect.

Some simulations are not applicable to certain classes: such as NSString and NSDate, these 'toll-free bridged' classes, otherwise an exception will be thrown.

Some methods cannot be stubbed: such as init, class, methodSignatureForSelector, forwardInvocation, etc.

Class methods of NSString and NSArray cannot be stubbed, otherwise they will be invalid.

Method calls of NSObject cannot be verified unless overridden in a subclass.

Private method calls of Apple core classes cannot be verified, such as methods that start with _.

Delayed verification of method calls is not supported, and only delayed verification in the expectation-run-verification mode is temporarily supported.

OCMock does not support multithreading.

3. Finally

I hope this article and examples have clarified some of the most common uses of OCMock. OCMock Website:http://ocmock.org/features/It is the best place to learn OCMock. Mocking is monotonous, but it is essential for an application. If a method is difficult to test with mock, this indicates that your design needs to be reconsidered.

Reference Links:

OCMock Official Website:https://ocmock.org/features/

OCMock3 Reference:https://www.cnblogs.com/xilifeng/p/4690280.html#header-c18

iOS Testing Series:http://blog.oneinbest.com/2017/07/27/iOS%20Testing%20Series%20-%203%20-%20OCMock%20Usage/

Author: JD Retail Wang Zhongwen

Source: JD Cloud Developer Community

你可能想看:
最后修改时间:
admin
上一篇 2025年03月28日 10:37
下一篇 2025年03月28日 11:00

评论已关闭