Advanced iOS Development (6)-Unit Tests (Unit Tests and UI Tests)

version

Xcode 11.5

1. Concept

1.1 Unit test

Unit testing refers to the inspection and verification of the smallest testable unit in the software. There are two types of unit tests (Unit Tests and UI Tests) in Xcode. Unit Tests are used to test functional modules; UI Tests are used to test UI interaction.

  • Unit Tests are used to test functional modules. These functional modules should be as single as possible to avoid coupling with other functions. For example, testing a function with a larger size, a function requesting a network, and so on.
  • UI Tests are used for UI interaction. It can realize functions such as automatic clicking on a button, view, or automatic text input by writing code or recording the developer's manual operation process and coding it.

1.2 Test case

Refers to the test code we use to test a certain function or UI interaction.

1.3 Affirmation

The main function of assertion is to allow developers to catch an error more conveniently, make the program crash, and report an error message. If an assertion fails, the program will report an error and freeze the line where the assertion is located.
Assertions only start in debug mode Function, will be ignored in release mode.
Some commonly used assertions:

XCTAssertNil(expression, ...) expression为空时通过, ...可填入报错信息
XCTAssertNotNil(expression, ...)  expression不为空时通过
XCTAssert(expression, ...)  expression为true时通过
XCTAssertTrue(expression, ...)  expression为true时通过
XCTAssertFalse(expression, ...)  expression为false时通过
XCTAssertEqual(expression1, expression2, ...)  expression1 = expression2 时通过
XCTAssertEqualObjects(expression1, expression2, ...)  expression1 = expression2 时通过
XCTAssertNotEqualObjects(expression1, expression2, ...)  expression1 != expression2 时通过

2. Preparation

Create a new project, and check Include Unit Tests and Include UI Tests, and then the system will automatically create unit test targets and template codes.

create.png

If the two unit tests are not checked when creating a new project, we can also add them later. Click the TARGET button to add:

create2.png

Then find those two unit tests:

create3.png

After adding, you can see the test code block:

tests.png

Here, the code block folder of Unit Tests is named project name + Tests; and the code block folder of UI Tests is named project name + UITests.
And each folder has its own default plist file created for the test class, and the test class has only .m file instead of .h file, because the unit test does not need to be called externally, all our testing work is done in the .m file.
A test class (.m file) can write many test cases. But if you test There are too many use cases, we can create multiple test classes in order to classify and manage these test cases. For example, there are those specifically used to test algorithm functions, some are specifically used to test various network request functions, and some are specifically used to test various functions of tool classes. Is it normal, etc.
Create a new test class:

create4.png

E.g:

ms.png

3. Unit Tests

The test class is inherited from XCTestCase, and the following sample code is given at the beginning of the system:

- (void)setUp {
    
    
    // 测试用例开始前执行 (初始化)
}

- (void)tearDown {
    
    
    // 测试用例结束后执行 (清理工作)
}

- (void)testExample {
    
    
    // 测试用例
}

- (void)testPerformanceExample {
    
    
    // 性能测试
    [self measureBlock:^{
    
    
        // 性能测试对象 (代码段)
    }];
}

Our test case must start with test in order to be recognized as a test case by the system. There is a diamond-shaped box in front of the test case method. Clicking on this will execute the test case. If the test passes, it will hit V, and if it fails, it will hit X. We can also use ⌘U to test all use cases of the current class.

Example 1
Test a simple ratio function to see if the test result is correct.

#import "KKAlgorithm.h"

// 最大值
- (void)testMaxValue {
    
    
    
    int value1 = 5;
    int value2 = 10;
    int maxInt = [KKAlgorithm maxValueWithValue1:value1 value2:value2];
        
    XCTAssertEqual(maxInt, value2);
//    XCTAssertEqual(maxInt, value1, @"返回最大值错误!");
}

maxInt=value2=10, the test passed. If you change value2 to value1, the test fails, and the program reports an error and freezes in the row of XCTAssertEqual.

KKAlgorithm.h

@interface KKAlgorithm : NSObject

// 获取最大值
+ (int)maxValueWithValue1:(int)value1 value2:(int)value2;

@end

KKAlgorithm.m

// 获取最大值
+ (int)maxValueWithValue1:(int)value1 value2:(int)value2 {
    
    
    
    return value1 > value2 ? value1 : value2;
}

Example 2
Requesting the network. Because the requesting network is asynchronous, we need to wait for the network to return before judging the test result. So this example will introduce a test waiting method.
Waiting method:

    XCTWaiter *waiter = [[XCTWaiter alloc] initWithDelegate:self];
    XCTestExpectation *expectation1 = [[XCTestExpectation alloc] initWithDescription:@"请求网络1"];
    XCTestExpectation *expectation2 = [[XCTestExpectation alloc] initWithDescription:@"请求网络2"];
    // 异步模拟请求网络1
    dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(2.0*NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
    
    
        [expectation1 fulfill];     // 满足期望
    });
    // 异步模拟请求网络2
    dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(2.0*NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
    
    
        [expectation2 fulfill];     // 满足期望
    });
    // 等待10秒, 如果expectation1和expectation2都满足期望, 则继续往下执行
    [waiter waitForExpectations:@[expectation1, expectation2] timeout:10.0];

XCTestExpectation test expectation, which is equivalent to waiting condition.
XCTWaiter is used to initiate waiting, and you can set waiting for multiple test expectations. There is a proxy method, but it is not used here so it is not explained in detail.

Demo to request data on Baidu homepage:

#import "KKHttp.h"

@interface KKTestsDemoTests : XCTestCase <KKHttpDelegate> {
    
    
    
    XCTestExpectation *_expectation;
}

// 网络测试
- (void)testHttp {
    
    
    
    XCTWaiter *waiter = [[XCTWaiter alloc] initWithDelegate:self];
    _expectation = [[XCTestExpectation alloc] initWithDescription:@"请求百度首页数据"];
    
    // 发起网络请求
    KKHttp *http = [[KKHttp alloc] init];
    http.delegate = self;
    [http fetchBaidu];
    
    // 等待10秒, 如果expectation满足期望, 则继续往下执行
    [waiter waitForExpectations:@[_expectation] timeout:10.0];
}

// 代理回调: 网络返回
- (void)http:(KKHttp *)http receiveData:(NSData *)data error:(NSError *)error {
    
    
    
    XCTAssertNotNil(data, @"网络无响应");
    NSString *dataStr = [[NSString alloc] initWithData:data encoding:NSUTF8StringEncoding];
    NSLog(@"dataStr:%@", dataStr);  // XML数据, 这里没进行解析
    
    [_expectation fulfill];     // 结束等待
}

Because the test expects XCTestExpectation to be satisfied in the proxy callback, the expectation object is set to the global variable XCTestExpectation *_expectation;.

KKHttp.h

#import <Foundation/Foundation.h>

NS_ASSUME_NONNULL_BEGIN

@class KKHttp;
@protocol KKHttpDelegate <NSObject>
@optional
- (void)http:(KKHttp *)http receiveData:(nullable NSData *)data error:(nullable NSError *)error;
@end

@interface KKHttp : NSObject

@property (nonatomic, weak) id<KKHttpDelegate>  delegate;

- (void)fetchBaidu;

@end

NS_ASSUME_NONNULL_END

KKHttp.m

#import "KKHttp.h"

@implementation KKHttp

- (void)fetchBaidu {
    
    
    
    NSURLRequest *request = [NSURLRequest requestWithURL:[NSURL URLWithString:@"https://www.baidu.com"] cachePolicy:NSURLRequestUseProtocolCachePolicy timeoutInterval:10.0];
    NSURLSession *session = [NSURLSession sharedSession];
    NSURLSessionDataTask *task = [session dataTaskWithRequest:request completionHandler:^(NSData * _Nullable data, NSURLResponse * _Nullable response, NSError * _Nullable error) {
    
    
        if ([self.delegate respondsToSelector:@selector(http:receiveData:error:)]) {
    
    
            [self.delegate http:self receiveData:data error:error];
        }
    }];
    [task resume];
}

@end

Example 3
The waiting method of Example 2 is very troublesome after all. If there are more test cases, a lot of duplicate code will be generated. The following discussion uses the notification method to wait, and then the notification is made into the form of a macro, which is convenient to call.
Macro pre-pass:

// 异步测试
- (void)testAlbumAuthorization {
    
    
       
    // 异步任务
    dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(1.0 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
    
    
        NSLog(@"222");
        // 发送通知: 结束等待
        [[NSNotificationCenter defaultCenter] postNotificationName:@"KKExpectationNotification" object:nil];
    });
    
    // 等待
    [self expectationForNotification:@"KKExpectationNotification" object:nil handler:nil];
    [self waitForExpectationsWithTimeout:5.0 handler:nil];
    NSLog(@"111");
}

Note that
expectationForNotification:object:handler: and waitForExpectationsWithTimeout:handler: are executed before postNotificationName:object:. In other words, make sure to print 111 and then 222, otherwise it will not achieve our expected results.

We write the wait and end wait in the form of macros, which is convenient for other test cases to call:

#define WAIT \
    [self expectationForNotification:@"KKExpectationNotification" object:nil handler:nil]; \
    [self waitForExpectationsWithTimeout:5.0 handler:nil];

#define NOTIFY \
    [[NSNotificationCenter defaultCenter] postNotificationName:@"KKExpectationNotification" object:nil];

// 异步测试
- (void)testAlbumAuthorization {
    
    
       
    // 异步任务
    dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(1.0 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
    
    
        NSLog(@"222");
        // 发送通知: 结束等待
        NOTIFY
    });
    
    // 等待
    WAIT
    NSLog(@"111");
}

ps
Sometimes when importing other objects in the test object, the system will prompt that the header file cannot be found:

Nofound.png

At this time, we can add the required header files in the TARGET–>Build settings–>Header Search Paths of the unit test. For example:

headers.png

4. UI Tests

Let's first take a look at the effect we want to achieve:

run.gif

Create two VCs: VC1 and VC2. Enter the account and password in VC1 and click login, jump to VC2, and then click the Back button of VC2 to return to VC1. Repeat this 10,000 times to test whether our login API has any problems.

In the test case of UI Tests, throw the cursor into the test case code area, and then click the small red circle to start recording the App interface.

record.png

Every time we interact with the App screen, the code area will automatically generate the corresponding code:

- (void)testExample {
    
    
    
    // 运行App
    XCUIApplication *app = [[XCUIApplication alloc] init];
    [app launch];

    
    XCUIApplication *app = [[XCUIApplication alloc] init];
    XCUIElement *element = [[[[[app childrenMatchingType:XCUIElementTypeWindow] elementBoundByIndex:0] childrenMatchingType:XCUIElementTypeOther].element childrenMatchingType:XCUIElementTypeOther].element childrenMatchingType:XCUIElementTypeOther].element;
    [[[element childrenMatchingType:XCUIElementTypeTextField] elementBoundByIndex:0] tap];
    
    XCUIElement *aKey = app/*@START_MENU_TOKEN@*/.keys[@"a"]/*[[".keyboards.keys[@\"a\"]",".keys[@\"a\"]"],[[[-1,1],[-1,0]]],[0]]@END_MENU_TOKEN@*/;
    [aKey tap];
    [aKey tap];
    [aKey tap];
    [aKey tap];
    [[[element childrenMatchingType:XCUIElementTypeTextField] elementBoundByIndex:1] tap];
    
    XCUIElement *bKey = app/*@START_MENU_TOKEN@*/.keys[@"b"]/*[[".keyboards.keys[@\"b\"]",".keys[@\"b\"]"],[[[-1,1],[-1,0]]],[0]]@END_MENU_TOKEN@*/;
    [bKey tap];
    [bKey tap];
    [bKey tap];
    [bKey tap];
    [app/*@START_MENU_TOKEN@*/.staticTexts[@"\U767b\U5f55"]/*[[".buttons[@\"\\U767b\\U5f55\"].staticTexts[@\"\\U767b\\U5f55\"]",".staticTexts[@\"\\U767b\\U5f55\"]"],[[[-1,1],[-1,0]]],[0]]@END_MENU_TOKEN@*/ tap];
    [app.buttons[@"Back"] tap];
    
}

Of course these codes are raw, we need to make some modifications:

- (void)testExample {
    
    
    
    // 运行App
    XCUIApplication *app = [[XCUIApplication alloc] init];
    [app launch];

    for (int i=0; i<10; i++) {
    
    
        
        // 界面中的元素 (控件)
        XCUIElement *element = [[[[[app childrenMatchingType:XCUIElementTypeWindow] elementBoundByIndex:0] childrenMatchingType:XCUIElementTypeOther].element childrenMatchingType:XCUIElementTypeOther].element childrenMatchingType:XCUIElementTypeOther].element;
        
        // 找到第一个输入框, 并点击
        [[[element childrenMatchingType:XCUIElementTypeTextField] elementBoundByIndex:0] tap];
        
        sleep(1);   // 等待键盘弹出
        
        // 点击键盘
        XCUIElement *aKey = app/*@START_MENU_TOKEN@*/.keys[@"a"]/*[[".keyboards.keys[@\"a\"]",".keys[@\"a\"]"],[[[-1,1],[-1,0]]],[0]]@END_MENU_TOKEN@*/;
        [aKey tap];
        
        // 找到第二个输入框, 并点击
        [[[element childrenMatchingType:XCUIElementTypeTextField] elementBoundByIndex:1] tap];
        
        // 点击键盘
        XCUIElement *bKey = app/*@START_MENU_TOKEN@*/.keys[@"b"]/*[[".keyboards.keys[@\"b\"]",".keys[@\"b\"]"],[[[-1,1],[-1,0]]],[0]]@END_MENU_TOKEN@*/;
        [bKey tap];
        
        // 找到登录按钮, 并点击
        [app.staticTexts[@"\U0000767b\U00005f55"] tap];
//        [app.staticTexts[@"登录"] tap];
        
        sleep(2);   // 等待加载VC2
        
        // 这时候已经跳转到VC2了
        // 找到Back按钮, 并点击
        [app.buttons[@"Back"] tap];
    }
}

note:

  1. In some places, use sleep to wait for the interface to load, otherwise an error will be reported if the control is not found on the screen;
  2. The Chinese login button is automatically generated @"\U767b\U5f55", we can manually add four 0s or write directly in Chinese.

Guess you like

Origin blog.csdn.net/u012078168/article/details/106841308