集成测试以检查内存泄漏

我们编写了许多单元测试,为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) { // some business logic and then... delegate.operationDidComplete() } } 


由于已经在实施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 { //     ,   ... public func make() -> UIViewController { makeAndExpose().view } typealias ModuleComponents = ( view: UIViewController, presenter: Presenter, Interactor: Interactor ) func makeAndExpose() -> ModuleComponents { // Set up code, and then... return ( view: viewController, presenter: presenter, interactor: interactor ) } } 


这解决了缺少对对象数据的直接访问的问题。 显然,这不是理想的选择,但是它可以满足我们的需求,因此让我们继续编写测试。 它看起来像这样:

 final class ModuleMemoryLeakTests: XCTestCase { //      .     //    . private var view: UIViewController? //        //   ,    // UIKit,   UIViewController  . private weak var presenter: Presenter? private weak var interactor: Interactor? //   setUp    ModuleFactory  //   makeAndExpose.     ,   //     ModuleComponents // ,          . //     . func setUp() { super.setUp() let moduleFactory = ModuleFactory(/* mocked dependencies & config */) let components = moduleFactory.makeAndExpose() view = components.view presenter = components.presenter interactor = components.interactor } //   ,   tearDown   , //        ,     ,   //     . func tearDown() { view = nil presenter = nil interactor = nil super.tearDown() } func test_module_doesNotLeakMemory() { //   ,      . //      ,  //          setUp. XCTAssertNotNil(presenter) XCTAssertNotNil(interactor) //        . //    ,   //     ,    //      . view = nil // ,  ,    //  Presenter  Interactor   . //  ,       //  ,    . XCTAssertNil(presenter) XCTAssertNil(interactor) } } 


因此,我们有一种简单的方法来检测VIPER模块中的内存泄漏。 这绝不是理想的选择,并且需要为每个我们要测试的新模块进行特定的用户工作,但这肯定比为每个可能的内存泄漏编写单独的单元测试要少得多。 它还有助于识别我们甚至不怀疑的内存泄漏。 实际上,在编写了其中的一些测试后,我们发现我们有一个未通过的测试,经过一些研究,我们发现模块中存在内存泄漏。 校正后,应重复测试。

这也为我们编写模块的更通用的集成测试集提供了起点。 最后,如果我们只是保持与Presenter的牢固链接并用模拟替换UIViewController ,则可以伪造用户输入,然后调用presenter方法并检查View中数据的虚拟显示。

Source: https://habr.com/ru/post/zh-CN459220/


All Articles