深圳幻海软件技术有限公司 欢迎您!

浅析iOS单元测试

2023-02-27

 单元测试作为敏捷开发实践的组成之一,其目的是提高软件开发的效率,维持代码的健康性。其目标是证明软件能够正常运行,而不是发现bug(发现bug这一目的与开发成本是正相关的,虽然发现bug是保证软件质量的一种手段,但是很显然这与降低软件开发成本这一目的背道而驰)。单元测试是对软件质量的一种保

 单元测试作为敏捷开发实践的组成之一,其目的是提高软件开发的效率,维持代码的健康性。其目标是证明软件能够正常运行,而不是发现bug(发现bug这一目的与开发成本是正相关的,虽然发现bug是保证软件质量的一种手段,但是很显然这与降低软件开发成本这一目的背道而驰)。

单元测试是对软件质量的一种保证,例如重构之后我们需要保证软件产品的正常运行。而iOS非常幸运,苹果开发工具Xcode在创建项目是就能够自带XCTest,包含单元测试和UI测试,这次我们从两个方面讲一下单元测试。

一、 开发

如果创建工程时自带,则可以在工程项目文件中的TARGETS看到有一个对应工程名,以Tests结尾的项目,如果没有,也可以自己创建,点击下方的加号,输入test即可看到对应的bundle,这里我们选择Unit Testing Bundle。

创建后会多一个文件夹,在其中创建和刚创建的项目名称相同,在里面就可以创建每个文件的单元测试了。

对文件夹右键点击NewFile,选择Unit Test Case Class即可创建一个单元测试文件。

创建过程就到这里了。接下来针对单元测试一些使用进行简单描述。

首先单元测试是为了测试方法的可行性,所以需要断言来看是否正确,XCTest提供了许多断言可用,这里就列举一些常见的断言:

XCTAssertNotNil(expression, ...)

XCTAssertNil(expression, ...)

XCTAssertTrue(expression, ...)

XCTAssertFalse(expression, ...)

XCTAssertEqualObjects(expression1, expression2, ...)

XCTAssertEqual(expression1, expression2, ...)

XCTAssertGreaterThan(expression1, expression2, ...)

还有更多可以通过XCTestAssertions.h来查找。

这里的断言很容易理解,可以通过方法名释义,比如第一个XCTAssertNotNil就是必须表达式或对象不为空才能通过,否则测试失败。

接下来我们用一个简单的例子来看一下:

这儿有个简单的实现加法的类以及方法。

#import <Foundation/Foundation.h> 
 
NS_ASSUME_NONNULL_BEGIN 
 
@interface CalcMethod : NSObject 
 
+ (NSInteger)plus:(NSInteger)a andB:(NSInteger)b; 
 
@end 
 
NS_ASSUME 
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.
  • 11.

 

#import "CalcMethod.h" 
 
@implementation CalcMethod 
 
+ (NSInteger)plus:(NSInteger)a andB:(NSInteger)b { 
    return a + b; 

 
@end 
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.

实现它的单元测试

首先先新建单元测试文件,这个只有.m,没有头文件。

#import <XCTest/XCTest.h> 
 
@interface CalcMethodTests : XCTestCase 
 
@end 
 
@implementation CalcMethodTests 
 
- (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. 
    }]; 
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.
  • 11.
  • 12.
  • 13.
  • 14.
  • 15.
  • 16.
  • 17.
  • 18.
  • 19.
  • 20.
  • 21.
  • 22.
  • 23.
  • 24.
  • 25.
  • 26.

可以看到系统给出了几个方法,其中setUp是每个方法执行测试前会调用用来初始化一些参数;tearDown是每个方法执行完成之后实行的一些销毁方法;testExample是用来具体测试的方法,也可以自己定义,但必须以test开头;testPerformanceExample是用来测试性能的,放在measureBlock中,会运行10次测每次所使用的时间,一般用来或者大量计算所产生的耗时。

这里只是单纯的加法运算,就不需要了。添加测试方法:

- (void)testCalcMethod { 
    XCTAssertEqual([CalcMethod plus:1 andB:2], 3, @"1+2=3"); 

  • 1.
  • 2.
  • 3.

然后切换Scheme到对应的Test中,如果没有则可以在管理Scheme中添加,然后点击方法前的菱形块即可测试该方法,或者点command+u将所有测试文件和方法进行单元测试。

左侧切到测试栏,这样可以更便捷的点击测试,测试通过就会显示勾。

二、 常见问题处理

1. 在第一次执行单元测试时发现编译不过,报找不到类型

原因是单元测试对引用要求更严格,以前在编译中可能会直接通过,但单元测试不行,解决办法也很简单,对所有相关文件引入对应头文件即可。

这个只是针对上方的报错,也可能有更多的错误,需要自己进行配对。

如果有在App中有接入自己开发的Framework工程,并且要对Framework进行单元测试,接下来是针对Framework中的一些单元测试问题

2. 找不到第三方库或者pod的头文件

这是因为如果你的framework通过pod引入,那么pod头文件管理不需要你自己来处理,pod会处理完并集成到App中。但单元测试不行,所以需要对你自己的Framework以及单元测试的bundle添加headers。

切到工程文件的Build SettingsàHeader Search Paths,加入你自己对应Pod的头文件路径,包括单元测试的bundle与单元测试对应的Framework工程都需要添加。

3. 报Include of non-modular header inside framework module

这个仍旧在单元测试bundle的Build SettingsàAllow Non-modular Includes In Framework Modules,将此设置改为YES即可。

4. 使用pod集成后,App调试报找不到XCTest

这是因为pod包含文件太粗糙,使用**代替所有子目录,导致单元测试的.m都一起被包含到pod文件中。

解决办法一:精确pod需要的文件路径

解决办法二:规范单元测试文件命名,并在pod配置中排除

5. 如果要测一些网络请求或异步操作怎么办?

如果直接在test方法中写入一些异步方法,在回调中在进行断言,会发现无论正确与否都会直接通过。所以苹果也提供了一个为单元测试准备的异步阻断。

/*! 
 * @method -waitForExpectationsWithTimeout:handler: 
 * 
 * @param timeout 
 * The amount of time within which all expectations must be fulfilled. 
 * 
 * @param handler 
 * If provided, the handler will be invoked both on timeout or fulfillment of all 
 * expectations. Timeout is always treated as a test failure. 
 * 
 * @discussion 
 * -waitForExpectationsWithTimeout:handler: creates a point of synchronization in the flow of a 
 * test. Only one -waitForExpectationsWithTimeout:handler: can be active at any given time, but 
 * multiple discrete sequences of { expectations -> wait } can be chained together. 
 * 
 * -waitForExpectationsWithTimeout:handler: runs the run loop while handling events until all expectations 
 * are fulfilled or the timeout is reached. Clients should not manipulate the run 
 * loop while using this API. 
 */ 
- (void)waitForExpectationsWithTimeout:(NSTimeInterval)timeout handler:(nullable XCWaitCompletionHandler)handler; 
 
/*! 
 * @method -expectationForNotification:object:handler: 
 * 
 * @discussion 
 * A convenience method for asynchronous tests that observe NSNotifications from the default 
 * NSNotificationCenter. 
 * 
 * @param notificationName 
 * The notification to register for
 * 
 * @param objectToObserve 
 * The object to observe. 
 * 
 * @param handler 
 * Optional handler, /see XCNotificationExpectationHandler. If not provided, the expectation 
 * will be fulfilled by the first notification matching the specified name from the 
 * observed object. 
 * 
 * @return 
 * Creates and returns an expectation associated with the test case
 */ 
- (XCTestExpectation *)expectationForNotification:(NSNotificationName)notificationName  
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.
  • 11.
  • 12.
  • 13.
  • 14.
  • 15.
  • 16.
  • 17.
  • 18.
  • 19.
  • 20.
  • 21.
  • 22.
  • 23.
  • 24.
  • 25.
  • 26.
  • 27.
  • 28.
  • 29.
  • 30.
  • 31.
  • 32.
  • 33.
  • 34.
  • 35.
  • 36.
  • 37.
  • 38.
  • 39.
  • 40.
  • 41.
  • 42.
  • 43.

方法我单独提出来常用的两个,这样可以阻塞线程,并在收到通知或者超时后再继续,更多方法详见XCTestCase+AsynchronousTesting.h。

下面是使用方法:

(void)testAsync { 
     
    dispatch_async(dispatch_get_global_queue(0, 0), ^{ 
        // Do Some things async 
        // XCAssert 
         
        [[NSNotificationCenter defaultCenter] postNotificationName:@"UnitTestsNotify" object:nil]; 
    }); 
     
    do { 
        [self expectationForNotification:@"UnitTestsNotify" object:nil handler:nil]; 
        [self waitForExpectationsWithTimeout:30 handler:nil]; 
    } while (0); 
 

  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.
  • 11.
  • 12.
  • 13.
  • 14.
  • 15.

也可以使用宏定义将这方法简化,更直观,也更方便多次调用。

#define WAIT do {\  
[self expectationForNotification:@"UnitTestsNotify" object:nil handler:nil];\  
[self waitForExpectationsWithTimeout:30 handler:nil];\  
} while (0);  
  
#define NOTIFY \  
[[NSNotificationCenter defaultCenter] postNotificationName:@"UnitTestsNotify" object:nil];  
  
- (void)testAsync {  
      
    dispatch_async(dispatch_get_global_queue(0, 0), ^{  
        // Do Some things async  
        // XCAssert  
          
        NOTIFY  
    });  
      
    WAIT  
}  
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.
  • 11.
  • 12.
  • 13.
  • 14.
  • 15.
  • 16.
  • 17.
  • 18.
  • 19.

【本文是51CTO专栏机构“AiChinaTech”的原创文章,微信公众号( id: tech-AI)”】

戳这里,看该作者更多好文