51Testing软件测试论坛

标题: 为什么我说写好测试很重要(二) [打印本页]

作者: lsekfe    时间: 2021-8-9 10:09
标题: 为什么我说写好测试很重要(二)
 四、 单元测试下开发模式、技术框架选择
  单元测试是按照测试范围来划分的。TDD、BDD 是按照开发模式来划分的。因此就有各种排列组合,这里我们只关心单元测试下的 TDD、BDD 方案。
  在单元测试阶段,TDD 和 BDD 都可以适用。

  1. TDD
  TDD 强调不断的测试推动代码的开发,这样简化了代码,保证了代码质量。
  思想是在拿到一个新的功能时,首先思考该功能如何测试,各种测试用例、各种边界 case;然后完成测试代码的开发;最后编写相应的代码以满足、通过这些测试用例。
  TDD 开发过程类似下图:
[attach]133677[/attach]
先编写该功能的测试用例,实现测试代码。这时候去跑测试,是不通过的,也就是到了红色的状态。
  然后编写真正的功能实现代码。这时候去跑测试,测试通过,也就是到了绿色的状态。
  在测试用例的保证下,可以重构、优化代码。

  抛出一个问题:TDD 看上去很好,应该用它吗?
  这个问题不用着急回答,回答了也不会有对错之分。开发中经常是这样一个流程,新的需求出来后,先经过技术评审会议,确定宏观层面的技术方案、确定各个端的技术实现、使用的技术等,整理出开发文档、会议文档。工期评估后开始编码。事情这么简单吗?前期即使想的再充分、再细致,可能还是存在特殊 case 漏掉的情况,导致技术方案或者是技术实现的改变。如果采用 TDD,那么之前新功能给到后,就要考虑测试用例的设计、编写了测试代码,在测试用例的保证下再去实现功能。如果遇到了技术方案的变更,之前的测试用例要改变、测试代码实现要改变。可能新增的某个 case 导致大部分的测试代码和实现代码都要改变。

  如何开展 TDD
  新建一个工程,确保 “Include Unit Tests” 选项是选中的状态。
[attach]133678[/attach]
创建后的工程目录如下:
[attach]133679[/attach]
删除 Xcode 创建的测试模版文件 TDDDemoTests.m。
  假如我们需要设计一个人类,它具有吃饭的功能,且当他吃完后会说一句“好饱啊”。
  那么按照 TDD 我们先设计测试用例。假设有个 Person 类,有个对象方法叫做吃饭,吃完饭后会返回一个“好饱啊”的字符串。那测试用例就是:
[attach]133680[/attach]
实现测试用例代码。创建继承自 Unit Test Case class 的测试类,命名为 工程前缀+测试类名+Test,也就是 TDDPersonTest.m。
[attach]133681[/attach]
因为要测试 Person 类,所以在主工程中创建 Person 类。
  因为要测试人类在吃饭后说一句“好饱啊”。所以设想那个类目前只有一个吃饭的方法。于是在 TDDPersonTest.m 中创建一个测试函数 -(void)testReturnStatusStringWhenPersonAte;函数内容如下:
  1. - (void)testReturnStatusStringWhenPersonAte
  2. {
  3.     // Given
  4.     Person *somebody = [[Person alloc] init];
  5.    
  6.     // When
  7.     NSString *statusMessage = [somebody performSelector:@selector(eat)];
  8.    
  9.     // Then
  10.     XCTAssert([statusMessage isEqualToString:@"好饱啊"], @"Person 「吃饭后返回“好饱啊”」功能异常");
  11. }
复制代码
Xcode 下按快捷键 Command + U,跑测试代码发现是失败的。因为我们的 Person 类根本没实现相应的方法。
  从 TDD 开发过程可以看到,我们现在是红色的 “Fail” 状态。所以需要去 Person 类中实现功能代码。Person 类如下:
  1. #import "Person.h"

  2. @implementation Person

  3. - (NSString *)eat
  4. {
  5.     [NSThread sleepForTimeInterval:1];
  6.     return @"好饱啊";;
  7. }

  8. @end
复制代码
 再次运行,跑一下测试用例(Command + U 快捷键)。发现测试通过,也就是TDD 开发过程中的绿色 “Success” 状态。
  例子比较简单,假如情况需要,可以在 -(void)setUp 方法里面做一些测试的前置准备工作,在 -(void)tearDown 方法里做资源释放的操作。
  假如 eat 方法实现的不够漂亮。现在在测试用例的保证下,大胆重构,最后确保所有的 Unit Test case 通过即可。

  2. BDD
  相比 TDD,BDD 关注的是行为方式的设计,拿上述“人吃饭”举例说明。
  和 TDD 相比第1~4步骤相同。
  BDD 则需要先实现功能代码。创建 Person 类,实现 -(void)eat;方法。代码和上面的相同。
  BDD 需要引入好用的框架 Kiwi,使用 Pod 的方式引入。
  因为要测试人类在吃饭后说一句“好饱啊”。所以设想那个类目前只有一个吃饭的方法。于是在 TDDPersonTest.m 中创建一个测试函数 -(void)testReturnStatusStringWhenPersonAte;函数内容如下:
  1. #import "kiwi.h"
  2. #import "Person.h"

  3. SPEC_BEGIN(BDDPersonTest)

  4. describe(@"Person", ^{
  5.     context(@"when someone ate", ^{
  6.         it(@"should get a string",^{
  7.          Person *someone = [[Person alloc] init];
  8.             NSString *statusMessage = [someone eat];
  9.             [[statusMessage shouldNot] beNil];
  10.             [[statusMessage should] equal:@"好饱啊"];
  11.         });
  12.     });
  13. });

  14. SPEC_EN
复制代码
3. XCTest
  开发步骤
  Xcode 自带的测试系统是 XCTest,使用简单。开发步骤如下:
  在 Tests 目录下为被测的类创建一个继承自 XCTestCase 的测试类。
  删除新建的测试代码模版里面的无用方法 - (void)testPerformanceExample、- (void)testExample。
  跟普通类一样,可以继承,可以写私有属性、私有方法。所以可以在新建的类里面,根据需求写一些私有属性等。
  在 - (void)setUp 方法里面写一些初始化、启动设置相关的代码。比如测试数据库功能的时候,写一些数据库连接池相关代码。
  为被测类里面的每个方法写测试方法。被测类里面可能是 n 个方法,测试类里面可能是 m 个方法(m >= n),根据我们在第三部分:单元测试编码规范里讲过的 一个测试用例只测试一个分支,方法内部有 if、switch 语句时,需要为每个分支写测试用例。
  为测试类每个方法写的测试方法有一定的规范。命名必须是 test+被测方法名。函数无参数、无返回值。比如 - (void)testSharedInstance。
  测试方法里面的代码按照 Given->When->Then 的顺序展开。测试环境所需的先决条件准备;调用所要测试的某个方法、函数;使用断言验证输出和行为是否符合预期。
  在 - (void)tearDown 方法里面写一些释放掉资源或者关闭的代码。比如测试数据库功能的时候,写一些数据库连接池关闭的代码。


  断言相关宏:
  1. /*!
  2. * [url=home.php?mod=space&uid=10594]@Function[/url] XCTFail(...)
  3. * Generates a failure unconditionally.
  4. * @param ... An optional supplementary description of the failure. A literal NSString, optionally with string format specifiers. This parameter can be completely omitted.
  5. */
  6. #define XCTFail(...) \
  7.     _XCTPrimitiveFail(self, __VA_ARGS__)

  8. /*!
  9. * @define XCTAssertNil(expression, ...)
  10. * Generates a failure when ((\a expression) != nil).
  11. * @param expression An expression of id type.
  12. * @param ... An optional supplementary description of the failure. A literal NSString, optionally with string format specifiers. This parameter can be completely omitted.
  13. */
  14. #define XCTAssertNil(expression, ...) \
  15.     _XCTPrimitiveAssertNil(self, expression, @#expression, __VA_ARGS__)

  16. /*!
  17. * @define XCTAssertNotNil(expression, ...)
  18. * Generates a failure when ((\a expression) == nil).
  19. * @param expression An expression of id type.
  20. * @param ... An optional supplementary description of the failure. A literal NSString, optionally with string format specifiers. This parameter can be completely omitted.
  21. */
  22. #define XCTAssertNotNil(expression, ...) \
  23.     _XCTPrimitiveAssertNotNil(self, expression, @#expression, __VA_ARGS__)

  24. /*!
  25. * @define XCTAssert(expression, ...)
  26. * Generates a failure when ((\a expression) == false).
  27. * @param expression An expression of boolean type.
  28. * @param ... An optional supplementary description of the failure. A literal NSString, optionally with string format specifiers. This parameter can be completely omitted.
  29. */
  30. #define XCTAssert(expression, ...) \
  31.     _XCTPrimitiveAssertTrue(self, expression, @#expression, __VA_ARGS__)

  32. /*!
  33. * @define XCTAssertTrue(expression, ...)
  34. * Generates a failure when ((\a expression) == false).
  35. * @param expression An expression of boolean type.
  36. * @param ... An optional supplementary description of the failure. A literal NSString, optionally with string format specifiers. This parameter can be completely omitted.
  37. */
  38. #define XCTAssertTrue(expression, ...) \
  39.     _XCTPrimitiveAssertTrue(self, expression, @#expression, __VA_ARGS__)

  40. /*!
  41. * @define XCTAssertFalse(expression, ...)
  42. * Generates a failure when ((\a expression) != false).
  43. * @param expression An expression of boolean type.
  44. * @param ... An optional supplementary description of the failure. A literal NSString, optionally with string format specifiers. This parameter can be completely omitted.
  45. */
  46. #define XCTAssertFalse(expression, ...) \
  47.     _XCTPrimitiveAssertFalse(self, expression, @#expression, __VA_ARGS__)

  48. /*!
  49. * @define XCTAssertEqualObjects(expression1, expression2, ...)
  50. * Generates a failure when ((\a expression1) not equal to (\a expression2)).
  51. * @param expression1 An expression of id type.
  52. * @param expression2 An expression of id type.
  53. * @param ... An optional supplementary description of the failure. A literal NSString, optionally with string format specifiers. This parameter can be completely omitted.
  54. */
  55. #define XCTAssertEqualObjects(expression1, expression2, ...) \
  56.     _XCTPrimitiveAssertEqualObjects(self, expression1, @#expression1, expression2, @#expression2, __VA_ARGS__)

  57. /*!
  58. * @define XCTAssertNotEqualObjects(expression1, expression2, ...)
  59. * Generates a failure when ((\a expression1) equal to (\a expression2)).
  60. * @param expression1 An expression of id type.
  61. * @param expression2 An expression of id type.
  62. * @param ... An optional supplementary description of the failure. A literal NSString, optionally with string format specifiers. This parameter can be completely omitted.
  63. */
  64. #define XCTAssertNotEqualObjects(expression1, expression2, ...) \
  65.     _XCTPrimitiveAssertNotEqualObjects(self, expression1, @#expression1, expression2, @#expression2, __VA_ARGS__)

  66. /*!
  67. * @define XCTAssertEqual(expression1, expression2, ...)
  68. * Generates a failure when ((\a expression1) != (\a expression2)).
  69. * @param expression1 An expression of C scalar type.
  70. * @param expression2 An expression of C scalar type.
  71. * @param ... An optional supplementary description of the failure. A literal NSString, optionally with string format specifiers. This parameter can be completely omitted.
  72. */
  73. #define XCTAssertEqual(expression1, expression2, ...) \
  74.     _XCTPrimitiveAssertEqual(self, expression1, @#expression1, expression2, @#expression2, __VA_ARGS__)

  75. /*!
  76. * @define XCTAssertNotEqual(expression1, expression2, ...)
  77. * Generates a failure when ((\a expression1) == (\a expression2)).
  78. * @param expression1 An expression of C scalar type.
  79. * @param expression2 An expression of C scalar type.
  80. * @param ... An optional supplementary description of the failure. A literal NSString, optionally with string format specifiers. This parameter can be completely omitted.
  81. */
  82. #define XCTAssertNotEqual(expression1, expression2, ...) \
  83.     _XCTPrimitiveAssertNotEqual(self, expression1, @#expression1, expression2, @#expression2, __VA_ARGS__)

  84. /*!
  85. * @define XCTAssertEqualWithAccuracy(expression1, expression2, accuracy, ...)
  86. * Generates a failure when (difference between (\a expression1) and (\a expression2) is > (\a accuracy))).
  87. * @param expression1 An expression of C scalar type.
  88. * @param expression2 An expression of C scalar type.
  89. * @param accuracy An expression of C scalar type describing the maximum difference between \a expression1 and \a expression2 for these values to be considered equal.
  90. * @param ... An optional supplementary description of the failure. A literal NSString, optionally with string format specifiers. This parameter can be completely omitted.
  91. */
  92. #define XCTAssertEqualWithAccuracy(expression1, expression2, accuracy, ...) \
  93.     _XCTPrimitiveAssertEqualWithAccuracy(self, expression1, @#expression1, expression2, @#expression2, accuracy, @#accuracy, __VA_ARGS__)

  94. /*!
  95. * @define XCTAssertNotEqualWithAccuracy(expression1, expression2, accuracy, ...)
  96. * Generates a failure when (difference between (\a expression1) and (\a expression2) is <= (\a accuracy)).
  97. * @param expression1 An expression of C scalar type.
  98. * @param expression2 An expression of C scalar type.
  99. * @param accuracy An expression of C scalar type describing the maximum difference between \a expression1 and \a expression2 for these values to be considered equal.
  100. * @param ... An optional supplementary description of the failure. A literal NSString, optionally with string format specifiers. This parameter can be completely omitted.
  101. */
  102. #define XCTAssertNotEqualWithAccuracy(expression1, expression2, accuracy, ...) \
  103.     _XCTPrimitiveAssertNotEqualWithAccuracy(self, expression1, @#expression1, expression2, @#expression2, accuracy, @#accuracy, __VA_ARGS__)

  104. /*!
  105. * @define XCTAssertGreaterThan(expression1, expression2, ...)
  106. * Generates a failure when ((\a expression1) <= (\a expression2)).
  107. * @param expression1 An expression of C scalar type.
  108. * @param expression2 An expression of C scalar type.
  109. * @param ... An optional supplementary description of the failure. A literal NSString, optionally with string format specifiers. This parameter can be completely omitted.
  110. */
  111. #define XCTAssertGreaterThan(expression1, expression2, ...) \
  112.     _XCTPrimitiveAssertGreaterThan(self, expression1, @#expression1, expression2, @#expression2, __VA_ARGS__)

  113. /*!
  114. * @define XCTAssertGreaterThanOrEqual(expression1, expression2, ...)
  115. * Generates a failure when ((\a expression1) < (\a expression2)).
  116. * @param expression1 An expression of C scalar type.
  117. * @param expression2 An expression of C scalar type.
  118. * @param ... An optional supplementary description of the failure. A literal NSString, optionally with string format specifiers. This parameter can be completely omitted.
  119. */
  120. #define XCTAssertGreaterThanOrEqual(expression1, expression2, ...) \
  121.     _XCTPrimitiveAssertGreaterThanOrEqual(self, expression1, @#expression1, expression2, @#expression2, __VA_ARGS__)

  122. /*!
  123. * @define XCTAssertLessThan(expression1, expression2, ...)
  124. * Generates a failure when ((\a expression1) >= (\a expression2)).
  125. * @param expression1 An expression of C scalar type.
  126. * @param expression2 An expression of C scalar type.
  127. * @param ... An optional supplementary description of the failure. A literal NSString, optionally with string format specifiers. This parameter can be completely omitted.
  128. */
  129. #define XCTAssertLessThan(expression1, expression2, ...) \
  130.     _XCTPrimitiveAssertLessThan(self, expression1, @#expression1, expression2, @#expression2, __VA_ARGS__)

  131. /*!
  132. * @define XCTAssertLessThanOrEqual(expression1, expression2, ...)
  133. * Generates a failure when ((\a expression1) > (\a expression2)).
  134. * @param expression1 An expression of C scalar type.
  135. * @param expression2 An expression of C scalar type.
  136. * @param ... An optional supplementary description of the failure. A literal NSString, optionally with string format specifiers. This parameter can be completely omitted.
  137. */
  138. #define XCTAssertLessThanOrEqual(expression1, expression2, ...) \
  139.     _XCTPrimitiveAssertLessThanOrEqual(self, expression1, @#expression1, expression2, @#expression2, __VA_ARGS__)

  140. /*!
  141. * @define XCTAssertThrows(expression, ...)
  142. * Generates a failure when ((\a expression) does not throw).
  143. * @param expression An expression.
  144. * @param ... An optional supplementary description of the failure. A literal NSString, optionally with string format specifiers. This parameter can be completely omitted.
  145. */
  146. #define XCTAssertThrows(expression, ...) \
  147.     _XCTPrimitiveAssertThrows(self, expression, @#expression, __VA_ARGS__)

  148. /*!
  149. * @define XCTAssertThrowsSpecific(expression, exception_class, ...)
  150. * Generates a failure when ((\a expression) does not throw \a exception_class).
  151. * @param expression An expression.
  152. * @param exception_class The class of the exception. Must be NSException, or a subclass of NSException.
  153. * @param ... An optional supplementary description of the failure. A literal NSString, optionally with string format specifiers. This parameter can be completely omitted.
  154. */
  155. #define XCTAssertThrowsSpecific(expression, exception_class, ...) \
  156.     _XCTPrimitiveAssertThrowsSpecific(self, expression, @#expression, exception_class, __VA_ARGS__)

  157. /*!
  158. * @define XCTAssertThrowsSpecificNamed(expression, exception_class, exception_name, ...)
  159. * Generates a failure when ((\a expression) does not throw \a exception_class with \a exception_name).
  160. * @param expression An expression.
  161. * @param exception_class The class of the exception. Must be NSException, or a subclass of NSException.
  162. * @param exception_name The name of the exception.
  163. * @param ... An optional supplementary description of the failure. A literal NSString, optionally with string format specifiers. This parameter can be completely omitted.
  164. */
  165. #define XCTAssertThrowsSpecificNamed(expression, exception_class, exception_name, ...) \
  166.     _XCTPrimitiveAssertThrowsSpecificNamed(self, expression, @#expression, exception_class, exception_name, __VA_ARGS__)

  167. /*!
  168. * @define XCTAssertNoThrow(expression, ...)
  169. * Generates a failure when ((\a expression) throws).
  170. * @param expression An expression.
  171. * @param ... An optional supplementary description of the failure. A literal NSString, optionally with string format specifiers. This parameter can be completely omitted.
  172. */
  173. #define XCTAssertNoThrow(expression, ...) \
  174.     _XCTPrimitiveAssertNoThrow(self, expression, @#expression, __VA_ARGS__)

  175. /*!
  176. * @define XCTAssertNoThrowSpecific(expression, exception_class, ...)
  177. * Generates a failure when ((\a expression) throws \a exception_class).
  178. * @param expression An expression.
  179. * @param exception_class The class of the exception. Must be NSException, or a subclass of NSException.
  180. * @param ... An optional supplementary description of the failure. A literal NSString, optionally with string format specifiers. This parameter can be completely omitted.
  181. */
  182. #define XCTAssertNoThrowSpecific(expression, exception_class, ...) \
  183.     _XCTPrimitiveAssertNoThrowSpecific(self, expression, @#expression, exception_class, __VA_ARGS__)

  184. /*!
  185. * @define XCTAssertNoThrowSpecificNamed(expression, exception_class, exception_name, ...)
  186. * Generates a failure when ((\a expression) throws \a exception_class with \a exception_name).
  187. * @param expression An expression.
  188. * @param exception_class The class of the exception. Must be NSException, or a subclass of NSException.
  189. * @param exception_name The name of the exception.
  190. * @param ... An optional supplementary description of the failure. A literal NSString, optionally with string format specifiers. This parameter can be completely omitted.
  191. */
  192. #define XCTAssertNoThrowSpecificNamed(expression, exception_class, exception_name, ...) \
  193.     _XCTPrimitiveAssertNoThrowSpecificNamed(self, expression, @#expression, exception_class, exception_name, __VA_ARGS__)
复制代码
经验小结
  XCTestCase 类和其他类一样,你可以定义基类,这里面封装一些常用的方法。
  1. // HCTTestCase.h
  2. #import <XCTest/XCTest.h>

  3. NS_ASSUME_NONNULL_BEGIN

  4. [url=home.php?mod=space&uid=10061]@interface[/url] HCTTestCase : XCTestCase

  5. @property (nonatomic, assign) NSTimeInterval networkTimeout;


  6. /**
  7. 用一个默认时间设置异步测试 XCTestExpectation 的超时处理
  8. */
  9. - (void)waitForExpectationsWithCommonTimeout;

  10. /**
  11. 用一个默认时间设置异步测试的

  12. @param handler 超时的处理逻辑
  13. */
  14. - (void)waitForExpectationsWithCommonTimeoutUsingHandler:(XCWaitCompletionHandler __nullable)handler;


  15. /**
  16. 生成 Crash 类型的 meta 数据

  17. [url=home.php?mod=space&uid=26358]@return[/url] meta 类型的字典
  18. */
  19. - (NSDictionary *)generateCrashMetaDataFromReport;

  20. @end

  21. NS_ASSUME_NONNULL_END

  22. // HCTTestCase.m
  23. #import "HCTTestCase.h"
  24. #import ...

  25. @implementation HCTTestCase

  26. #pragma mark - life cycle

  27. - (void)setUp
  28. {
  29.     [super setUp];
  30.     self.networkTimeout = 20.0;
  31.     // 1. 设置平台信息
  32.     [self setupAppProfile];
  33.     // 2. 设置 Mget 配置
  34.     [[TITrinityInitManager sharedInstance] setup];
  35.     // ....
  36.     // 3. 设置 HermesClient
  37.     [[HermesClient sharedInstance] setup];
  38. }

  39. - (void)tearDown
  40. {
  41.     [super tearDown];
  42. }


  43. #pragma mark - public Method

  44. - (void)waitForExpectationsWithCommonTimeout
  45. {
  46.     [self waitForExpectationsWithCommonTimeoutUsingHandler:nil];
  47. }

  48. - (void)waitForExpectationsWithCommonTimeoutUsingHandler:(XCWaitCompletionHandler __nullable)handler
  49. {
  50.     [self waitForExpectationsWithTimeout:self.networkTimeout handler:handler];
  51. }


  52. - (NSDictionary *)generateCrashMetaDataFromReport
  53. {
  54.     NSMutableDictionary *metaDictionary = [NSMutableDictionary dictionary];
  55.     NSDate *crashTime = [NSDate date];
  56.     metaDictionary[@"MONITOR_TYPE"] = @"appCrash";
  57.     // ...
  58.     metaDictionary[@"USER_CRASH_DATE"] = @([crashTime timeIntervalSince1970] * 1000);
  59.     return [metaDictionary copy];
  60. }


  61. #pragma mark - private method

  62. - (void)setupAppProfile
  63. {
  64.     [[CMAppProfile sharedInstance] setMPlatform:@"70"];
  65.     // ...
  66. }

  67. @end
复制代码
上述说的基本是开发规范相关。测试方法内部如果调用了其他类的方法,则在测试方法内部必须 Mock 一个外部对象,限制好返回值等。
  在 XCTest 内难以使用 mock 或 stub,这些是测试中非常常见且重要的功能。

  例子
  这里举个例子,是测试一个数据库操作类 HCTDatabase,代码只放某个方法的测试代码。
  1. - (void)testRemoveLatestRecordsByCount
  2. {
  3.     XCTestExpectation *exception = [self expectationWithDescription:@"测试数据库删除最新数据功能"];
  4.     // 1. 先清空数据表
  5.     [dbInstance removeAllLogsInTableType:HCTLogTableTypeMeta];
  6.     // 2. 再插入一批数据
  7.     NSMutableArray *insertModels = [NSMutableArray array];
  8.     NSMutableArray *reportIDS = [NSMutableArray array];
  9.    
  10.     for (NSInteger index = 1; index <= 100; index++) {
  11.         HCTLogMetaModel *model = [[HCTLogMetaModel alloc] init];
  12.         model.log_id = index;
  13.         // ...
  14.         if (index > 90 && index <= 100) {
  15.             [reportIDS addObject:model.report_id];
  16.         }
  17.         [insertModels addObject:model];
  18.     }
  19.     [dbInstance add:insertModels inTableType:HCTLogTableTypeMeta];
  20.    
  21.     // 3. 将早期的数据删除掉(id > 90 && id <= 100)
  22.     [dbInstance removeLatestRecordsByCount:10 inTableType:HCTLogTableTypeMeta];
  23.    
  24.     // 4. 拿到当前的前10条数据和之前存起来的前10条 id 做比较。再判断当前表中的总记录条数是否等于 90
  25.     [dbInstance getLatestRecoreds:10 inTableType:HCTLogTableTypeMeta completion:^(NSArray<HCTLogModel *> * _Nonnull records) {
  26.         NSArray<HCTLogModel *> *latestRTentRecords = records;
  27.         
  28.         [dbInstance getOldestRecoreds:100 inTableType:HCTLogTableTypeMeta completion:^(NSArray<HCTLogModel *> * _Nonnull records) {
  29.             NSArray<HCTLogModel *> *currentRecords = records;
  30.             
  31.             __block BOOL isEarlyData = NO;
  32.             [latestRTentRecords enumerateObjectsUsingBlock:^(HCTLogModel * _Nonnull obj, NSUInteger idx, BOOL * _Nonnull stop) {
  33.                 if ([reportIDS containsObject:obj.report_id]) {
  34.                     isEarlyData = YES;
  35.                 }
  36.             }];
  37.             
  38.             XCTAssert(!isEarlyData && currentRecords.count == 90, @"***Database「删除最新n条数据」功能:异常");
  39.             [exception fulfill];
  40.         }];
  41.         
  42.     }];
  43.     [self waitForExpectationsWithCommonTimeout];
  44. }
复制代码
3. 测试框架
  1)Kiwi
  BDD 框架里的 Kiwi 可圈可点。使用 CocoaPods 引入 pod 'Kiwi'。看下面的例子。
  被测类(Planck 项目是一个基于 WebView 的 SDK,根据业务场景,发现针对 WebView 的大部分功能定制都是基于 WebView 的生命周期内发生的,所以参考 NodeJS 的中间件思想,设计了基于生命周期的 WebView 中间件)。
  1. #import <Foundation/Foundation.h>

  2. @interface TPKTrustListHelper : NSObject

  3. +(void)fetchRemoteTrustList;

  4. +(BOOL)isHostInTrustlist:(NSString *)scheme;

  5. +(NSArray *)trustList;

  6. @end
复制代码
测试类:
  1. SPEC_BEGIN(TPKTrustListHelperTest)
  2. describe(@"Middleware Wrapper", ^{
  3.    
  4.     context(@"when get trustlist", ^{
  5.         it(@"should get a array of string",^{
  6.             NSArray *array = [TPKTrustListHelper trustList];
  7.             [[array shouldNot] beNil];
  8.             NSString *first = [array firstObject];
  9.             [[first shouldNot] beNil];
  10.             [[NSStringFromClass([first class]) should] equal:@"__NSCFString"];
  11.         });
  12.     });
  13.    
  14.     context(@"when check a string wether contained in trustlist ", ^{
  15.         it(@"first string should contained in trustlist",^{
  16.             NSArray *array = [TPKTrustListHelper trustList];
  17.             NSString *first = [array firstObject];
  18.             [[theValue([TPKTrustListHelper isHostInTrustlist:first]) should] equal:@(YES)];
  19.         });
  20.     });
  21. });
  22. SPEC_END
复制代码
例子包含 Kiwi 的最基础元素。SPEC_BEGIN 和 SPEC_END 表示测试类;describe 描述需要被测试的类;context 表示一个测试场景,也就是 Given->When->Then 里的 Given;it 表示要测试的内容,也就是也就是 Given->When->Then 里的 When 和 Then。1个 describe 下可以包含多个 context,1个 context 下可以包含多个 it。
  Kiwi 的使用分为:Specs、 Expectations 、 Mocks and Stubs 、Asynchronous Testing 四部分。
  it 里面的代码块是真正的测试代码,使用链式调用的方式,简单上手。
  测试领域中 Mock 和 Stub 非常重要。Mock 模拟对象可以降低对象之间的依赖,模拟出一个纯净的测试环境(类似初中物理课上“控制变量法”的思想)。Kiwi 也支持的非常好,可以模拟对象、模拟空对象、模拟遵循协议的对象等等,点击  Mocks and Stubs 查看。Stub 存根可以控制某个方法的返回值,这对于方法内调用别的对象的方法返回值很有帮助。减少对于外部的依赖,单一测试当前行为是否符合预期。
  针对异步测试,XCTest 则需要创建一个 XCTestExpectation 对象,在异步实现里面调用该对象的 fulfill 方法,最后设置最大等待时间和完成的回调 - (void)waitForExpectationsWithTimeoutNSTimeInterval)timeout handlernullable XCWaitCompletionHandler)handler; 如下例子:
  1. XCTestExpectation *exception = [self expectationWithDescription:@"测试数据库插入功能"];
  2.     [dbInstance removeAllLogsInTableType:HCTLogTableTypeMeta];
  3.     NSMutableArray *insertModels = [NSMutableArray array];
  4.     for (NSInteger index = 1; index <= 10000; index++) {
  5.         HCTLogMetaModel *model = [[HCTLogMetaModel alloc] init];
  6.         model.log_id = index;
  7.      // 。。。
  8.         [insertModels addObject:model];
  9.     }
  10.     [dbInstance add:insertModels inTableType:HCTLogTableTypeMeta];
  11.     [dbInstance recordsCountInTableType:HCTLogTableTypeMeta completion:^(NSInteger count) {
  12.         XCTAssert(count == insertModels.count, @"**Database「数据增加」功能:异常");
  13.         [exception fulfill];
  14.     }];
  15.     [self waitForExpectationsWithCommonTimeout];
复制代码
2)expecta、Specta
  expecta 和 Specta 都出自 orta 之手,他也是 Cocoapods 的开发者之一。太牛逼了,工程化、质量保证领域的大佬。
  Specta 是一个轻量级的 BDD 测试框架,采用 DSL 模式,让测试更接近于自然语言,因此更易读。
  特点:
  易于集成到项目中。在 Xcode 中勾选 Include Unit Tests ,和 XCTest 搭配使用。
  语法很规范,对比 Kiwi 和 Specta 的文档,发现很多东西都是相同的,也就是很规范,所以学习成本低、后期迁移到其他框架很平滑。
  Expecta 是一个匹配(断言)框架,相比 Xcode 的断言 XCAssert,Excepta 提供更加丰富的断言。
  特点:
  Eepecta 没有数据类型限制,比如 1,并不关心是 NSInteger 还是 CGFloat链式编程,写起来很舒服。
  反向匹配,很灵活。断言匹配用 except(...).to.equal(...),断言不匹配则使用 .notTo 或者 .toNot
  延时匹配,可以在链式表达式后加入 .will、.willNot、.after(interval) 等。

  4.小结
  Xcode 自带的 XCTestCase 比较适合 TDD,不影响源代码,系统独立且不影响 App 包大小。适合简单场景下的测试。且每个函数在最左侧又个测试按钮,点击后可以单独测试某个函数。
  Kiwi 是一个强大的 BDD 框架,适合稍微复杂写的项目,写法舒服、功能强大,模拟对象、存根语法、异步测试等满足几乎所有的测试场景。不能和 XCTest 继承。
  Specta 也是一个 BDD 框架,基于 XCTest 开发,可以和 XCTest 模版集合使用。相比 Kiwi,Specta 轻量一些。开发中一般搭配 Excepta 使用。如果需要使用 Mock 和 Stud 可以搭配 OCMock。
  Excepta 是一个匹配框架,比 XCTest 的断言则更加全面一些。
  没办法说哪个最好、最合理,根据项目需求选择合适的组合。
















欢迎光临 51Testing软件测试论坛 (http://bbs.51testing.com/) Powered by Discuz! X3.2