0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 1 year has passed since last update.

XCTestを利用してメモリーリーク検出を自動化する。

Last updated at Posted at 2023-08-31

環境

OS: macOS 13.4.1
Xcode.app: Version 14.3.1 (14E300c)
動作iOSバージョン:14.0以上

XCTestを利用してメモリーリーク検出する。

メモリーリークを引き起こすViewControllerを用意する。

retain cycleが起きるようなコードを加えたViewControllerを作成します。

class LeakingViewController: UIViewController {

    lazy var button = UIButton(primaryAction: UIAction(handler: { _ in
        self.onButtonAction()
    }))

    override func viewDidLoad() {
        view.addSubview(button)
    }

    func onButtonAction() {
        print("")
    }
}

メモリーリーク検出を行うコードを追加する。

addTeardownBlockはドキュメントからテストが終わった後に実行するブロックを登録するとあるのでviewControllerにメモリーリークがなければ、参照は残らずにnilになるはずなのでXCTAssertNilをかけてnilであれば"Object should be deallocated. Detected memory leak."というメッセージが出るようにします。

addTeardownBlock

func testExample() throws {
    let viewController = LeakingViewController()
    _ = viewController.view
    addTeardownBlock { [weak viewController] in
        XCTAssertNil(viewController, "Object should be deallocated. Detected memory leak.")
    }
}

テストを実行します。下記のように検出できました。

Result001

LeakingViewControllerのコードを修正します。

lazy var button = UIButton(primaryAction: UIAction(handler: { [weak self] _ in
    self?.onButtonAction()
}))

テストを実行します。
テストが成功しメッセージは消えます。

テスト対象を増やした場合

検出はうまくいきましたがテスト対象が増えた時はどうなるのでしょうか?。
テスト項目を増やしてみます。

Presenterを持つViewControllerにretain cycleが起きるようなコードを加えました。

protocol LeakingViewControllerDelegate: AnyObject {
    func buttonClicked()
}

protocol LoadingView {
    func startLoading()
}

class LeakingViewControllerPresenter: LeakingViewControllerDelegate {
    var view: LoadingView

    init(view: LoadingView) {
        self.view = view
    }

    func buttonClicked() {
        view.startLoading()
        //do some operation ...
    }
}

class LeakingViewControllerTwo: UIViewController, LoadingView {

    var delegate: LeakingViewControllerDelegate?

    lazy var button = UIButton(primaryAction: UIAction(handler: { [weak self] _ in
        self?.delegate?.buttonClicked()
    }))

    override func viewDidLoad() {
        view.addSubview(button)
    }

    func startLoading() {
        print("show view")
    }
}

addTeardownBlockを利用してテストコードを記述します。

func testExampleTwo() throws {
    let viewController = LeakingViewControllerTwo()
    let presenter = LeakingViewControllerPresenter(view: viewController)
    viewController.delegate = presenter
    _ = viewController.view
    addTeardownBlock { [weak viewController, weak presenter] in
        XCTAssertNil(viewController, "Object should be deallocated. Detected memory leak.")
        XCTAssertNil(presenter, "Object should be deallocated. Detected memory leak.")
    }
}

XCTestCaseにメモリーリーク検出用拡張機能を追加する。

テストを実行するとテスト対象が一つの時と同じように表示できますが、テスト対象が増えるたびにブロック内のコード量が増えてしまいます。

そこで下記のようにXCTestCaseにextensionを用意してみました。
メッセージの共通化に加えて、ファイルと行を追加して、テストが失敗した正確な行とファイルに失敗メッセージが表示されるようにします。

extension XCTestCase {
    func trackForMemoryLeak(instance: AnyObject,
                            file: StaticString = #filePath,
                            line: UInt = #line) {
        addTeardownBlock { [weak instance] in
            XCTAssertNil(
                instance,
                "potential memory leak on \(String(describing: instance))",
                file: file,
                line: line
            )
        }
    }
}

実際に利用すると下記のような結果を表示できます。

Result002

今回のデモはこちらで公開していますのでご参考になればと思います。

参考

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?