我们编写了许多单元测试,为iOS开发了
SoundCloud应用程序。 单元测试看起来非常漂亮。 它们简短(希望)可读,并且使我们确信我们编写的代码可以按预期工作。 但是,顾名思义,单元测试仅覆盖一个代码块,通常是一个函数或类。 那么,如何捕获类之间交互中存在的错误-诸如
内存泄漏之类的错误?
内存泄漏
有时很难检测到内存泄漏错误。 可能会强烈引用该委托,但也存在更难以发现的错误。 例如,以下代码是否很明显可能包含内存泄漏?
final class UseCase { weak var delegate: UseCaseDelegate? private let service: Service init(service: Service) { self.service = service } func run() { service.makeRequest(handleResponse) } private func handleResponse(response: ServiceResponse) {
由于已经在实施
Service ,因此不能保证其行为。 通过将
handleResponse函数传递给捕获
自身的私有函数,我们为
Service提供了对
UseCase 的强烈引用。 如果
Service决定保留此链接-并且我们不能保证不会发生这种情况-则会发生内存泄漏。 但是,通过粗略地研究代码,并不能真正做到这一点。
John Sandell也有一篇精彩的
文章,关于使用单元测试来检测类的内存泄漏。 但是,在上面的示例中,很容易跳过内存泄漏,但并不总是清楚如何编写这样的单元测试。 (当然,我们不是在经验方面讲。)
正如Guilherme在
最近的一篇
文章中所写,iOS的SoundCloud应用程序中的新功能是根据“干净的架构模式”编写的-通常是
VIPER类型。 这些
VIPER模块中的大多数都是使用我们称为
ModuleFactory构建的。 这样的
ModuleFactory需要一些输入,依赖关系和配置-并创建一个
UIViewController ,该
UIViewController已经连接到模块的其余部分,并且可以
推送到导航堆栈中。
该
VIPER模块可能具有多个委托,观察者和失控故障,从控制器中将其从导航堆栈中移除后,每种故障都可能导致控制器保留在内存中。 发生这种情况时,内存量将增加,并且操作系统很可能决定停止应用程序。
那么通过编写尽可能少的单元测试是否可以覆盖这么多潜在的泄漏? 如果没有,那么所有这些都是浪费时间。
整合测试
从这篇文章的标题可能会猜到答案是肯定的。 我们通过集成测试来做到这一点。 集成测试的目的是测试对象之间的交互方式。 当然,
VIPER模块是一组对象,内存泄漏是我们绝对希望避免的一种交互形式。
我们的计划很简单:我们将使用
ModuleFactory实例化
VIPER模块。 然后,我们将删除指向
UIViewController的链接,并确保该模块的所有重要部分均已销毁。
我们面临的第一个问题是,从本质上讲,我们不能轻易访问
VIPER模块的除
UIViewController之外的任何部分。
ModuleFactory中唯一的
公共函数是
func make()-> UIViewController 。 但是,如果我们仅为测试添加另一个入口点怎么办? 这个新方法将通过
internal声明,因此我们只能通过
@testable importing ModuleFactory框架来访问它。 它将返回到模块所有最重要部分的链接,然后我们可以保留这些链接,以使弱链接可以进入测试。 最终看起来像这样:
public final class ModuleFactory {
这解决了缺少对对象数据的直接访问的问题。 显然,这不是理想的选择,但是它可以满足我们的需求,因此让我们继续编写测试。 它看起来像这样:
final class ModuleMemoryLeakTests: XCTestCase {
因此,我们有一种简单的方法来检测
VIPER模块中的内存泄漏。 这绝不是理想的选择,并且需要为每个我们要测试的新模块进行特定的用户工作,但这肯定比为每个可能的内存泄漏编写单独的单元测试要少得多。 它还有助于识别我们甚至不怀疑的内存泄漏。 实际上,在编写了其中的一些测试后,我们发现我们有一个未通过的测试,经过一些研究,我们发现模块中存在内存泄漏。 校正后,应重复测试。
这也为我们编写模块的更通用的集成测试集提供了起点。 最后,如果我们只是保持与
Presenter的牢固链接并用
模拟替换
UIViewController ,则可以伪造用户输入,然后调用presenter方法并检查
View中数据的虚拟显示。