单元测试(一)
前言:
单元测试 Unit Test
单元测试:是检查每个代码单元(例如类或函数)是否能产生预期
的结果。 单元测试是独立运行的,不依赖于其他模块或组件。
UI测试
UI测试:属于端到端测试,是从应用程序启动到结束的测试过程。
UI测试:完全按照用户与应用程序交互的方式来复制与应用程序的交 互。
UI测试:比单元测试慢得多,运行起来也更消耗资源。
测试应涵盖:
- 核心功能:模型类和方法及其与控制器的交互 2. UI工作流程
- 特殊的边界条件
- Bug处理
测试原则 FIRST:
- Fast:测试模块应该是快速高效的;
- Independent/Isolated:测试模块应该是独立相互不影响的;
- Repeatable:测试实例应该是可以重复使用的,测试结果应该是相同的;
- Self-validating:测试应完全自动化。输出结果要么是“成功”,要么是“失败”;
- Timely:理想情况下,应该在编写要测试的生产代码之前编写测试(测试驱动开发)。
测试驱动开发
- TDD Test-Driven Development
- BDD Behavior-Driven Development
三个步骤:
- Arrange:构建数据,描述代码场景;
- Act:编写代码;
- Assert:检查代码是否达到预期效果
一. 单元测试 Unit Test
首先创建带有单元测试的工程LoginApp,创建完成之后,在文件AppDelegate.m的didFinishLaunchingWithOptions方法中打断点,Cmd + U执行单元测试,会发现断点执行到didFinishLaunchingWithOptions方法
得出结论: 单元测试需要启动App,需要执行didFinishLaunchingWithOptions方法
此时会有一个问题,didFinishLaunchingWithOptions方法中需要添加启动页 定位 等相关业务功能,此时执行单元测试didFinishLaunchingWithOptions方法中的业务相关代码也会执行,这就导致单元测试会非常耗时。
接下来探索避免单元测试的耗时问题?
新建FakeAppDelegate类
// FakeAppDelegate.h文件内容
#import <UIKit/UIKit.h>
NS_ASSUME_NONNULL_BEGIN
@interface FakeAppDelegate : UIResponder <UIApplicationDelegate>
@end
NS_ASSUME_NONNULL_END
// FakeAppDelegate.m文件内容
#import "FakeAppDelegate.h"
@implementation FakeAppDelegate
- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {
// Override point for customization after application launch.
return YES;
}
@end
// 修改main.m内容如下,把需要加载的appdelegate修改为FakeAppDelegate
#import <UIKit/UIKit.h>
#import "AppDelegate.h"
int main(int argc, char * argv[]) {
NSString * appDelegateClassName;
@autoreleasepool {
// Setup code that might create autoreleased objects goes here.
appDelegateClassName = NSStringFromClass([AppDelegate class]);
}
return UIApplicationMain(argc, argv, nil, @"FakeAppDelegate");
}
Cmd + U执行单元测试,发现会执行FakeAppDelegate中的方法
此时只要判断当前运行的是单元测试工程就加载FakeAppDelegate,如果运行的是LoginApp工程,就加载AppDelegate,这样就能避免单元测试的耗时问题
// Cmd + U运行单元测试的时候,单元测试工程LoginAppTests中创建的类都会进行编译,可以用下面方法判断当前运行的是单元测试还是LoginApp工程
#import <UIKit/UIKit.h>
#import "AppDelegate.h"
int main(int argc, char * argv[]) {
NSString * appDelegateClassName;
@autoreleasepool {
// 方法 字符串-》class
id cls = NSClassFromString(@"XCTest");
appDelegateClassName = cls ? @"FakeAppDelgate" : NSStringFromClass([AppDelegate class]);
}
return UIApplicationMain(argc, argv, nil, appDelegateClassName);
}
二. 链接XCTest动态库
Cmd + U 执行单元测试,下面打印信息
// 单元测试LoginAPPTests.m文件中testExample方法,底层是Test Case
// 每次执行testExample方法需要0.086秒
Test Case '-[LoginAppUnitTests testExample]' passed (0.086 seconds).
通过单元测试可以获取一些信息,比如编写一个Test Case,能够获取当前测试用例的执行时间,那么Targes下LoginAPPTests 是怎么测试到我们工程中的功能呢?
- Products文件下,选中LoginApp.app,右键show in finder
- 选中LoginAPPUITests-Runner,右键显示包内容
发现Frameworks下面多了几个动态库,是不是ipa包里面有了这几个动态库,就可以在项目中运行我们的测试工程?
接下来就给我们的工程引入XCTest动态库
// 在Xcode中寻找XCTest动态库
$ lldb -P
/Applications/Xcode.app/Contents/SharedFrameworks/LLDB.framework/Resources/Python3
$ open /Applications/Xcode.app/Contents
// XCTest动态库目录
Contents -> Developer -> Platforms -> iPhoneSimulator.platform(这里选择模拟器) -> Developer -> Library -> Frameworks -> XCTest.framework
找到了XCTest,就开始把XCTest引入我们的工程
创建Debug.Config.xcconfig文件,并进行相应配置
// 引入XCTest动态库
// 方式一
// 1. header
HEADER_SEARCH_PATHS = $(inherited) "/Applications/Xcode.app/Contents/Developer/Platforms/iPhoneSimulator.platform/Developer/Library/Frameworks/XCTest.framework/Headers"
// 2.链接动态库
// 传统方式
OTHER_LDFLAGS = $(inherited) -F"/Applications/Xcode.app/Contents/Developer/Platforms/iPhoneSimulator.platform/Developer/Library/Frameworks" -framework "XCTest"
// 3.配置rpath
LD_RUNPATH_SEARCH_PATHS = $(inherited) "/Applications/Xcode.app/Contents/Developer/Platforms/iPhoneSimulator.platform/Developer/Library/Frameworks"
// 方式二
// 1. header
HEADER_SEARCH_PATHS = $(inherited) "/Applications/Xcode.app/Contents/Developer/Platforms/iPhoneSimulator.platform/Developer/Library/Frameworks/XCTest.framework/Headers"
// 2.链接动态库
// 配置路径
SLASH = /
OTHER_LDFLAGS = $(inherited) ${SLASH}/Applications/Xcode.app/Contents/Developer/Platforms/iPhoneSimulator.platform/Developer/Library/Frameworks/XCTest.framework/XCTest
// 3.配置rpath
LD_RUNPATH_SEARCH_PATHS = $(inherited) "/Applications/Xcode.app/Contents/Developer/Platforms/iPhoneSimulator.platform/Developer/Library/Frameworks"
三. 执行XCTestCase
XCTest动态库引入之后,执行单元测试,ViewController.m文件内容修改如下
#import "ViewController.h"
// module
#import <XCTest/XCTest.h>
@interface LoginAppUITests : XCTestCase
@end
@implementation LoginAppUITests
- (void)setUp {
self.continueAfterFailure = NO;
}
- (void)testExample {
NSLog(@"------1223412");
}
@end
@interface ViewController ()
@end
@implementation ViewController
- (void)viewDidLoad {
[super viewDidLoad];
}
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
// 管理者 XCTestSuite
XCTestSuite *suite = [XCTestSuite defaultTestSuite];
// 初始化 testCase
LoginAppUITests *testCase = [LoginAppUITests new];
// testCase -》 suite
[suite addTest:testCase];
for (XCTest *test in suite.tests) {
[test runTest];
}
}
@end
// 运行工程,点击屏幕,正常打印
2021-03-14 22:22:20.839039+0800 LoginApp[41985:4221406] ------1223412
上面方式只能执行一次,第一次点击屏幕成功打印,第二次点击屏幕会崩溃,原因是[XCTestSuite defaultTestSuite] 这种方式只能执行一次。
接下来我们来学习第二种方式:这样就能避免只执行一次的问题,这样就可以边运行边测试
// 修改点击方法如下
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
// 管理者 XCTestSuite
XCTestSuite *suite = [XCTestSuite testSuiteForTestCaseClass:LoginAppUITests.class];
// 初始化 testCase
LoginAppUITests *testCase = [LoginAppUITests new];
// testCase -》 suite
[suite addTest:testCase];
for (XCTest *test in suite.tests) {
[test runTest];
}
}
// 如果运行时间过长,可以边运行边测试来进行相应优化
2021-03-14 22:35:47.512057+0800 LoginApp[42107:4233651] ------1223412
Test Case '-[LoginAppUITests testExample]' passed (0.005 seconds).
/*! 官方文档
* @class XCTestSuite
* A concrete subclass of XCTest, XCTestSuite is a collection of test cases. Suites
* are usually managed by the IDE, but XCTestSuite also provides API for dynamic test
* and suite management:
* @textblock
XCTestSuite *suite = [XCTestSuite testSuiteWithName:@"My tests"];
[suite addTest:[MathTest testCaseWithSelector:@selector(testAdd)]];
[suite addTest:[MathTest testCaseWithSelector:@selector(testDivideByZero)]]; */
// 第三种方式:
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
// @"MyTest"是固定写法
XCTestSuite *suite = [XCTestSuite testSuiteWithName:@"MyTest"];
// 指定单元测试类中某一个方法
[suite addTest:[LoginAppUITests testCaseWithSelector:@selector(testExample)]];
for (XCTest *test in suite.tests) {
[test runTest];
}
}