iOS中如何对具有复杂依赖的SDK在真机上进行单元测试

简介:

单元测试在软件开发中一直有着极其重要的地位,iOS的开发也不例外。随着App规模的不断膨胀,开发也逐渐的趋向模块化,开发者常常以库的形式封装功能,最后组成App。此时由于App结构变得复杂,各种库又可能存在着相互依赖的缘故,单元测试也随之变得复杂起来。开发者可能面临着一系列问题,比如:单元测试如何处理这些依赖?如何在真机上运行测试?如何在App所在的环境中运行测试?本文将用一个模拟的开发环境逐一进行讨论。

目录


在刚刚接触软件开发时,从未想过要写单元测试,总觉得自己写的代码质量很高,根本不需要测试。需要将宝贵的时间放到开发上,测试是测试人员的事情。后面才发现,经常因为一个小需求的增加,动了一处代码,结果其它地方出现重大问题,没测试到就上线了。甚至到了后面,代码复杂度越来越高,每动一处代码都提心吊胆,生怕有其它情形未考虑到,如履薄冰。经历了很多次惨痛教训之后才醒悟过来,单元测试是保证代码质量的不二法则。在《代码重构》一书中,每进行一步重构,作者都会先运行一遍单元测试,然后再进行后面的重构,因为只有这样,才能够保证重构之后代码的正确性,如果连正确都无法保证,重构有何意义?

Apple从Xcode 5开始,引入了最新的测试框架XCTest,非常完美的将测试与开发环境集成在了一起。关于如何使用XCTest,网上有非常多的介绍,大家可以看看Apple的官方文档,NSHipster也写过一篇文章:Unit Testing

随着开发者越来越重视单元测试,有人提出了TDD(测试驱动开发),并得到了很多开发者的推崇。这种思想会先根据需求或者接口来编写测试用例,然后才开始写业务代码,这样极大的保证了写出来的代码的正确性。关于在iOS上使用TDD,OneV写过一篇TDD的iOS开发初步以及Kiwi使用入门,有兴趣的可以去看看,这里不再展开介绍,本文集中讨论下面特定场景中的单元测试。

问题

iOS开发现在多数都使用CocoaPods进行第三方库的依赖管理,这样开发者们可以集中注意力放在自己模块的开发上面。比如著名的网络库AFNetworking。它的开源代码中也包含了单元测试,写得非常好,可以作为范例去学习。

但是由于AFNetworking本身的特点,决定了其单元测试环境其实是比较简单的,比如:

  • AFNetworking算是一个独立的库,并没有依赖其它的第三方库
  • 不依赖复杂的App环境
  • 不依赖真机环境

然而,很多时候,我们的开发环境比AFNetworking复杂得多,比如:

  • 依赖其它的第三方库,如何处理这些依赖的问题?
  • 依赖的某些第三方库又必须运行在复杂的App环境中,如何让测试运行于App环境?
  • 某些方法必须在真机上才能运行,如何让测试运行于真机上?

这些问题AFNetworking的测试用例都没有,而且默认创建的测试target都无法运行在这些环境中。如何利用XCTest来对以上复杂情形下的SDK进行单元测试?我们从模拟以上开发环境开始。

搭建SDK开发环境

首先我们来搭建一个满足以上复杂条件但却典型的开发环境:创建三个工程,其中ECApp是最终应用,它依赖了我们正在开发的ECSDK,而后者又依赖了第三方库EC3rdFramework。整个目录结构为:


.
├── EC3rdFramework
│   ├── EC3rdFramework.podspec
│   ├── EC3rdFramework.xcodeproj
│   ├── Podfile
│   ├── Sources
│   │   ├── ECFoo.h
│   │   └── ECFoo.m
│   └── SupportingFiles
├── ECApp
│   ├── ECApp
│   │   ├── AppDelegate.h
│   │   ├── AppDelegate.m
│   │   ├── Assets.xcassets
│   │   │   └── AppIcon.appiconset
│   │   │       └── Contents.json
│   │   ├── Base.lproj
│   │   │   ├── LaunchScreen.storyboard
│   │   │   └── Main.storyboard
│   │   ├── Info.plist
│   │   ├── ViewController.h
│   │   ├── ViewController.m
│   │   └── main.m
│   ├── ECApp.xcodeproj
│   └── Podfile
└── ECSDK
    ├── ECSDK.podspec
    ├── ECSDK.xcodeproj
    ├── Podfile
    ├── Sources
    │   ├── ECUsingFoo.h
    │   └── ECUsingFoo.m
    └── SupportingFiles


第三方库:EC3rdFramework

EC3rdFramework是我们开发的ECSDK所依赖的第三方库,其中包含一个ECFoo类,含有三个方法,分别模拟三种场景:

// ECFoo.m
// 模拟不依赖任何环境
- (BOOL)methodDependsOnNothing {
    return YES;
}

// 模拟依赖应用的环境
- (BOOL)methodDependsOnAppEnv {
    NSNumber *appInitialized = [[NSUserDefaults standardUserDefaults] objectForKey:@"AppInitialized"];
    if (appInitialized) {
        NSLog(@"running in app env");
        return YES;
    } else {
        NSLog(@"NOT running in app env");
        return NO;
    }
}

// 模拟依赖真实设备
- (BOOL)methodMustBeRunningOnDevice {
#if TARGET_IPHONE_SIMULATOR
    NSLog(@"running on simulator");
    return NO;
#else
    NSLog(@"running on device");
    return YES;
#endif
}

三个方法非常简单的模拟了三种典型的场景,满足条件时才会返回YES,代码很简单。对于依赖应用环境的场景,是通过App设置的一个标志位来判断,后面ECApp部分会看到这个标志位的设置。

其podspec如下:

Pod::Spec.new do |s|
  s.name                = "EC3rdFramework"
  s.version             = "1.0.0"
  s.requires_arc        = true
  s.source_files        = [ '**/Sources/**/*.h', '**/Sources/**/*.m']
  s.ios.deployment_target = '7.0'
end

开发中的SDK:ECSDK

ECSDK为我们所开发的SDK,它同EC3rdFramework一样,也是一个静态库,包含ECUsingFoo类,与前面的ECFoo类包含相同的方法,每个方法直接调用ECFoo中对应的方法,这样做是为了模拟依赖第三方库的场景

// ECUsingFoo.m
#import <EC3rdFramework/ECFoo.h>
// ...
- (BOOL)methodDependsOnNothing {
    ECFoo *foo = [ECFoo new];
    return [foo methodDependsOnNothing];
}

- (BOOL)methodDependsOnAppEnv {
    ECFoo *foo = [ECFoo new];
    return [foo methodDependsOnAppEnv];
}

- (BOOL)methodMustBeRunningOnDevice {
    ECFoo *foo = [ECFoo new];
    return [foo methodMustBeRunningOnDevice];
}

同前面类似,它的podspec如下,区别在于它多了对第三方库的依赖:

Pod::Spec.new do |s|
  s.name                = "ECSDK"
  s.version             = "1.0.0"
  s.requires_arc        = true
  s.source_files        = [ '**/Sources/**/*.h', '**/Sources/**/*.m']
  s.dependency          'EC3rdFramework'
  s.ios.deployment_target = '7.0'
end

也是因为这个依赖,还需要一个Podfile:

target "ECSDK" do
  pod 'EC3rdFramework', :path => '../EC3rdFramework'
end

最终应用:ECApp

ECApp为使用ECSDK的App,它启动之后立刻调用ECSDK中暴露的接口:

// AppDelegate.m
#import <ECSDK/ECUsingFoo.h>
// ...
- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {
    // Override point for customization after application launch.

    [[NSUserDefaults standardUserDefaults] setObject:[NSNumber numberWithBool:YES] forKey:@"AppInitialized"];
    [[NSUserDefaults standardUserDefaults] synchronize];

    ECUsingFoo *foo = [ECUsingFoo new];
    [foo methodDependsOnNothing];
    [foo methodDependsOnAppEnv];
    [foo methodMustBeRunningOnDevice];
    return YES;
}

方法的前两行,先设置了App环境的标识,前面ECFoo中便是依赖于此标识来判断是否处于App的环境中。

它的Podfile也很简单:

target "ECApp" do
  pod 'EC3rdFramework', :path => '../EC3rdFramework'
  pod 'ECSDK', :path => '../ECSDK'
end

这里有一点需要注意的是,实际上ECApp不会直接去依赖EC3rdFramework,它是被ECSDK依赖,按理说不需要加到Podfile中,CocoaPods会帮我们处理这种依赖。但由于EC3rdFramework并非已经发布的第三方库,如果不加上这一句的话,在pod install时会出现下面的错误:

[!] Unable to find a specification for EC3rdFramework depended upon by ECSDK

CocoaPods会去已经发布的库中去寻找,而不是本地。同时由于CocoaPods也不支持在podspec中像Podfile中一样,通过:path => ../EC3rdFramework指定本地路径,StackOverflow这里有讨论,所以采用这种变通方法。但这并不影响我们演示。

在ECApp路径下执行pod install之后,然后编译运行,将会得到以下日志:

2016-02-16 21:27:32.032 ECApp[30005:1064278] running in app env
2016-02-16 21:27:32.032 ECApp[30005:1064278] running on simulator

表示库已经正常调用,运行于App环境中的模拟器上。我们需要进行单元测试的开发环境搭建完成。

添加单元测试

环境搭好之后,接下来,为ECSDK添加单元测试。由于Xcode集成了XCTest,所以添加单元测试非常简单,依次选择菜单项:New/Target/iOS/Test/iOS Unit Testing Bundle,这里我们的测试target为ECSDKTests。完成后,在ECSDK工程中会生成对应的target和源文件,可以看到工程中有一个ECSDKTests.m文件,这是Xcode默认生成的测试用例,是来打酱油的,什么事都没做。选中ECSDKTests这个Scheme,按下⌘ U(注意,这里是U,而不是平时所用的B和R)编译并运行测试,因为此时是默认的空测试用例,所以测试很顺利的完成:

测试通过

编写测试用例

为了测试ECSDK中提供的方法,我们需要为其添加新的测试用例。三种场景,只有返回YES时才算通过测试,由此表示测试可以运行于这些环境中:

// ECSDKTests.m
#import "ECUsingFoo.h"
// ...
- (void)testMethodDependsOnNothing {
    ECUsingFoo *foo = [ECUsingFoo new];
    XCTAssert([foo methodDependsOnNothing], @"The method must be running in ANY env");
}

- (void)testMethodDependsOnAppEnv {
    ECUsingFoo *foo = [ECUsingFoo new];
    XCTAssert([foo methodDependsOnAppEnv], @"The method must be running in app env");
}

- (void)testMethodMustBeRunningOnDevice {
    ECUsingFoo *foo = [ECUsingFoo new];
    XCTAssert([foo methodMustBeRunningOnDevice], @"The method must be running on device");
}

测试用例很简单,我们来看看是否可以运行。再次选中ECSDKTests这个Scheme,⌘ U编译运行,此时出现以下错误:

Undefined symbols for architecture x86_64:
  "_OBJC_CLASS_$_ECUsingFoo", referenced from:
      objc-class-ref in ECSDKTests.o
ld: symbol(s) not found for architecture x86_64
clang: error: linker command failed with exit code 1 (use -v to see invocation)

错误信息显示链接时找不到ECUsingFoo方法,后者是定义在ECSDK工程中,表示测试target需要依赖ECSDK。处理依赖有多种方法:可以在Build Settings中添加库及其对应路径。还有一种更好的办法,利用CocoaPods,在它的Podfile中增加一个target即可,这样可以保证ECSDKTests和ECSDK的依赖完全一致。新的Podfile像这样:

def import_common_pods
    pod 'EC3rdFramework', :path => '../EC3rdFramework'
end

target "ECSDK", :exclusive => true do
    import_common_pods
end

target "ECSDKTests", :exclusive => true do
    import_common_pods
    pod 'ECSDK', :path => '.'
end

因为依赖了共同的库,所以将这抽出来成为一个单独的方法,接着在两个target中调用。由于测试target是依赖于ECSDK,所以还需要加上:pod 'ECSDK', :path => '.'。重新pod install⌘ U,编译问题解决,测试可以正常运行。

两个用例失败

但现在面临两个新的问题,因为现在测试只能运行于模拟器上,而且并非是App的环境,所以后面两个测试无法通过。

如果我们直接将Scheme选成真机上运行,一按⌘ U便会弹出以下错误提示:

> Logic Testing on iOS devices is not supported. You can run logic tests on the Simulator.

暂时无法运行于真机上。

让测试运行在App环境

我们先来看如何让测试运行于App环境中。Apple在开发文档中提过两个概念,一个叫Logic Tests,另外一个叫Application Tests,前者表示简单的逻辑测试,只能够运行在模拟器中,我们刚创建的测试target正是前面一种。这也是为何选择真机时,会弹出上面错误提示的原因。

文档中提到了如何配置Application Tests的方法,但是很遗憾,因为这篇文档是针对旧的OCTest框架,现在Xcode采用了新的XCTest框架,所以已经是”Retired”状态:

Retired Document

Important: This version of Unit Testing Guide has been retired. The replacement document focuses on the new testing features and workflow provided by Xcode 5 and later revisions. For information covering the same subject area as this page, please see Testing with Xcode.

新的文档中也没有再提这两个概念。但由于XCTest的前身就是OCTest,是否配置的方法也是相通的?是否将测试target变成Application Tests之后,就可以运行在App环境中?抱着试一试的想法,按照废弃文档中的方法来配置测试target。

在General配置页面,里面有一个Host Application,这个便表示测试是否可以运行于App中。但由于当前测试的是一个静态库,无法选择想要运行的App,此时需要用通过其它途径来指定。在ECSDKTests的Build Settings中修改两处:

  1. Bundle Loader: Your/App/Path/ECApp.app/ECApp
  2. Test host: $(BUNDLE_LOADER)

再次运行,发现ECApp的应用先启动,随后测试用例开始执行。因为ECApp在启动之后便配置了App环境的标志位,所以环境依赖的测试用例可以正常通过,测试已经可以运行于App的环境中,我们的尝试成功了。现在只剩下最后一个场景,如何让测试运行于真机上:

App环境测试成功

让测试运行在真机上

其实,在完成上一步的配置之后,测试已经从所谓的Logic Tests就转变成了Application Tests,而后者对运行的环境是没有限制的。直接将Scheme设置成真机,先编译一下ECApp,再运行一次测试,所有的测试可以通过:

设备测试成功

特殊情况

注意:事情并不会总是这么顺利,有时候由于一个App过于庞大,各个库的podspec写得不是很规范,不是所有依赖的Libraries都写在了podspec中,有些被放在Build Phases里面,系统库尤为常见。这样就导致即使我们按照前面介绍的都配置好了,还是无法让测试target编译通过,在链接时会出现各种各样的找不到符号的错误。此时需要手动去添加这些库到测试target的Build Phases中。至于需要添加哪些,只有根据编译时的错误逐一添加了。而且有一点需要注意:有时库的Status需要是Optional,否则最后链接的时候也会出错。下面是一个真实测试用例在Build Phases中所依赖的库:

系统库

它一共依赖了41个系统库,每一个都是在编译出错时,查到缺少的符号所在的库来添加的,是个体力活:-)。

结尾

至此,我们的测试用例已经可以运行于上面描述的几种典型的复杂环境,其实最重要的步骤只有两步,第一步是设置依赖,处理各种编译错误;第二步是设置Build Settings,将测试转成Application Tests,让测试能够运行于App环境。

我们搭建的环境和真实的环境相比起来,复杂度还存在一定的差距,在编译测试target时会出现各种各样奇怪的问题,本文无法一一例举,靠大家根据实际情况处理了。

如果想对Xcode的测试有一个系统的了解,强烈建议大家去阅读文档Testing with Xcode,非常详细的介绍了用Xcode进行测试的方方面面。

新的一年,以这篇简单的文章作为起始,祝大家新年快乐!

(全文完)

目录
相关文章
|
2月前
|
机器学习/深度学习 人工智能 监控
提升软件质量的关键路径:高效测试策略与实践在软件开发的宇宙中,每一行代码都如同星辰般璀璨,而将这些星辰编织成星系的过程,则依赖于严谨而高效的测试策略。本文将引领读者探索软件测试的奥秘,揭示如何通过精心设计的测试方案,不仅提升软件的性能与稳定性,还能加速产品上市的步伐,最终实现质量与效率的双重飞跃。
在软件工程的浩瀚星海中,测试不仅是发现缺陷的放大镜,更是保障软件质量的坚固防线。本文旨在探讨一种高效且创新的软件测试策略框架,它融合了传统方法的精髓与现代技术的突破,旨在为软件开发团队提供一套系统化、可执行性强的测试指引。我们将从测试规划的起点出发,沿着测试设计、执行、反馈再到持续优化的轨迹,逐步展开论述。每一步都强调实用性与前瞻性相结合,确保测试活动能够紧跟软件开发的步伐,及时适应变化,有效应对各种挑战。
|
2月前
|
人工智能 监控 JavaScript
模拟依赖关系和 AI 是Vue.js测试的下一个前沿领域
模拟依赖关系和 AI 是Vue.js测试的下一个前沿领域
35 1
|
4月前
|
测试技术 Linux 虚拟化
iOS自动化测试方案(五):保姆级VMware虚拟机安装MacOS
详细的VMware虚拟机安装macOS Big Sur的保姆级教程,包括下载VMware和macOS镜像、图解安装步骤和遇到问题时的解决方案,旨在帮助读者顺利搭建macOS虚拟机环境。
190 3
iOS自动化测试方案(五):保姆级VMware虚拟机安装MacOS
|
4月前
|
测试技术 开发工具 iOS开发
iOS自动化测试方案(三):WDA+iOS自动化测试解决方案
这篇文章是iOS自动化测试方案的第三部分,介绍了在没有MacOS系统条件下,如何使用WDA(WebDriverAgent)结合Python客户端库facebook-wda和tidevice工具,在Windows系统上实现iOS应用的自动化测试,包括环境准备、问题解决和扩展应用的详细步骤。
380 1
iOS自动化测试方案(三):WDA+iOS自动化测试解决方案
|
4月前
|
Java 测试技术 Maven
单元测试问题之在Maven项目中引入JUnit 5和Mockito的依赖如何解决
单元测试问题之在Maven项目中引入JUnit 5和Mockito的依赖如何解决
269 1
|
4月前
|
测试技术 数据安全/隐私保护 iOS开发
iOS自动化测试方案(四):保姆级搭建iOS自动化开发环境
iOS自动化测试方案的第四部分,涵盖了基础环境准备、iPhone虚拟机设置、MacOS虚拟机与iPhone真机的连接,以及扩展问题和代码示例,确保读者能够顺利完成环境搭建并进行iOS自动化测试。
436 0
iOS自动化测试方案(四):保姆级搭建iOS自动化开发环境
|
4月前
|
测试技术 虚拟化 iOS开发
iOS自动化测试方案(二):Xcode开发者工具构建WDA应用到iphone
这篇文章是iOS自动化测试方案的第二部分,详细介绍了在Xcode开发者工具中构建WebDriverAgent(WDA)应用到iPhone的全过程,包括环境准备、解决构建过程中可能遇到的错误,以及最终成功安装WDA到设备的方法。
241 0
iOS自动化测试方案(二):Xcode开发者工具构建WDA应用到iphone
|
4月前
|
测试技术 开发工具 虚拟化
iOS自动化测试方案(一):MacOS虚拟机保姆级安装Xcode教程
这篇文章提供了一份保姆级的教程,指导如何在MacOS虚拟机上安装Xcode,包括环境准备、基础软件安装以及USB扩展插件的使用,以实现iOS自动化测试方案的第一步。
233 0
iOS自动化测试方案(一):MacOS虚拟机保姆级安装Xcode教程
|
4月前
|
数据可视化 API 开发工具
影创SDK☀️三、工程默认配置,及基础测试建议
影创SDK☀️三、工程默认配置,及基础测试建议
|
4月前
|
测试技术 Swift iOS开发
探索iOS自动化测试:使用Swift编写UI测试
【8月更文挑战第31天】在软件开发的海洋中,自动化测试是保证船只不偏离航线的灯塔。本文将带领读者启航,深入探索iOS应用的自动化UI测试。我们将通过Swift语言,点亮代码的灯塔,照亮测试的道路。文章不仅会展示如何搭建测试环境,还会提供实用的代码示例,让理论知识在实践中生根发芽。无论你是新手还是有经验的开发者,这篇文章都将是你技能提升之旅的宝贵指南。