WWDC18 - Testing Tips and Tricks - 2(Working with notifications)

2023. 4. 12. 00:48IOS/WWDC

반응형

Working with notifications

  • Notification을 보내거나 옵저버가 제대로 작동하는지 여부를 테스트 하기 위해서는 테스트 할 Notification은 격리되어 있어야 온전한 테스트라고 할 수 있다.
  • Notification은 일대다 통신이므로 특정 부분만을 테스트를 시도하다 다른 Notificationpost를 받은 옵저버로 인해 의도하지 않은 부작용이 생길 수 있다. 그러므로, 격리 혹은 고립시켜서 테스트를 할 필요가 있다.
class PointsOfInterestTableViewController {
    var observer: AnyObject?
    var didHandleNotification = false

    init() {
        let name = CurrentLocationProvider.authChangedNotification
        observer = NotificationCenter.default.addObserver(forName: name, object: nil, queue: .main, using: { [weak self] _ in
            self?.handleAuthChanged()
        })
    }

    func handleAuthChanged() {
        didHandleNotification = true
    }
}
  • PointsOfInterestTableViewControllerCurrentLocationProviderNameNotification을 받아낸다.
  • 위 뷰 컨트롤러가 제대로 옵저빙 하는지 파악하기 위해서 간단하게 테스트코드를 작성하면 아래와 같이 작성할 수 있을것이다.
class PointsOfInterestTableViewControllerTests: XCTestCase {
    func testNotification() {
        let observer = PointsOfInterestTableViewController()

        XCTAssertFalse(observer.didHandleNotification)

        let name = CurrentLocationProvider.authChangedNotification
        NotificationCenter.default.post(name: name, object: nil)

        XCTAssertTrue(observer.didHandleNotification)
    }
}
  • 이는 뷰컨트롤러에서와 같은 NotificationCenter.defulat를 통하여 알림을 보내고 있다.
  • 이는 테스트코드로는 동작을 하지만 맨 처음에서 말한 고립된 환경은 아니다.
  • 만약 NotificationCenter.default에서 해당 name으로 여러개의 옵저버를 만들었다면 모든 곳에 post될 것이고 이는 테스트 속도를 저하시킨다.
  • 그러므로 우리는 테스트 코드뿐만 아니라 뷰 컨트롤러 역시 조금 더 세분화시켜야한다.

Testing Notification Observers

  • 우선 NotificationCenter가 여러 인스턴스를 가질 수 있음을 알고있어야 한다. default라는 기본 인스턴스가 제공되지만 필요할 경우 추가적으로 새로운 인스턴스를 만들 수 있으므로 테스트 환경을 구축하는데 큰 도움을 줄 수 있다.
  • 새롭게 만든 인스턴스를 통하여 default대신 사용하여야한다. 이는 흔히 종속성 주입(dependency injection)이라고 불린다.
class PointsOfInterestTableViewController {
    var observer: AnyObject?
    var didHandleNotification = false
    var notificationCenter: NotificationCenter

    init(notificationCenter: NotificationCenter = .default) {
        self.notificationCenter = notificationCenter
        let name = CurrentLocationProvider.authChangedNotification
        observer = NotificationCenter.default.addObserver(forName: name, object: nil, queue: .main, using: { [weak self] _ in
            self?.handleAuthChanged()
        })
    }

    func handleAuthChanged() {
        didHandleNotification = true
    }
}
  • NotificationCenter의 새로운 인스턴스를 만들고 init에 해당 값을 받도록 구현할 수 있다.
  • 만약 init(notificationCenter: NotificationCenter)로 구현을 한다면 앱이 동작하거나 앱을 처음 동작시킬 때 따로 NotificationCenter를 만들어 뷰컨트롤러 인스턴스를 만들어야겠지만 =.default를 주면서 기존 앱에서 큰 수정없이 테스트만을 위한 init을 만들 수 있다.
class PointsOfInterestTableViewControllerTests: XCTestCase {
    func testNotification() {
        let notificationCenter = NotificationCenter()
        let observer = PointsOfInterestTableViewController(notificationCenter: notificationCenter)

        XCTAssertFalse(observer.didHandleNotification)

        let name = CurrentLocationProvider.authChangedNotification
        notificationCenter.post(name: name, object: nil)

        XCTAssertTrue(observer.didHandleNotification)
    }
}
  • 위의 테스트코드를 약간 수정하자면 위와 같이 수정을 할 수 있다.
  • 이럴경우 기존의 default를 통해 받는 옵저버와 상관없이 고립된 환경에서의 옵저버가 만들어지며, post메소드가 제대로 동작하는지 여부를 쉽게 확인할 수 있다.

Testing Notification Posters

  • 위에서는 옵저버가 제대로 동작하는지 여부를 확인했다면, 이번에는 NotificationCenter가 제대로 post되는지 여부를 확인해봐야한다.
  • 이는 위에서 NotificationCenter를 종속성 주입을 통해 사용한것과 마찬가지의 방법을 사용할 것이고
  • XCTest가 제공하는 XCTNSNotificationExpectation를 사용하여 테스트 해 볼것이다.
class CurrentLocationProvider {
    static let authChangedNotification = Notification.Name("AuthChanged")

    func notifyAuthChanged() {
        let name = CurrentLocationProvider.authChangedNotification

        NotificationCenter.default.post(name: name, object: self)
    }
}
  • 이는 기존에 종속성 주입을 하기전의 뷰 컨트롤러와 비슷하다.
class CurrentLocationProviderTests: XCTestCase {
    func testNotifyAuthChanged() {
        let poster = CurrentLocationProvider()
        var observer: AnyObject?
        let expectation = self.expectation(description: "auth changed notification")

        let name = CurrentLocationProvider.authChangedNotification
        observer = Notification.default.addObserver(forName: name, object: poster, queue: .main, using: { _ in
            NotificationCenter.default.removeObserver(observer!)
            expectation.fulfill()
        })

        poster.notifyAuthChanged()
        wait(for: [expectation], timeout: 0)
    }
}
  • 위 코드를 XCTest가 제공하는 XCTNSNotificationExpectation메소드를 통해 우리는 조금 더 간편하게 만들 수 있다.
class CurrentLocationProviderTests: XCTestCase {
    func testNotifyAuthChanged() {
        let poster = CurrentLocationProvider()
        let name = CurrentLocationProvider.authChangedNotification
        let expectation = XCTNSNotificationExpectation(name: name, object: object)
        //....
    }
}
  • 하지만, 위에서 말한것과 같이 NotificationCenter.default를 쓰는것은 다른 옵저버에도 해당 post를 보내는 문제가 있기 때문에 NotificationCenter환경을 고립시킬 필요가 있다.
class CurrentLocationProvider {
    static let authChangedNotification = Notification.Name("AuthChanged")

    let notificationCenter: NotificationCenter

    init(notificationCenter: NotificationCenter = .default) {
        self.notificationCenter = notificationCenter
    }

    func notifyAuthChanged() {
        let name = CurrentLocationProvider.authChangedNotification

        notificationCenter.post(name: name, object: self)
    }
}
  • 뷰컨트롤러에서 해준 종속성 주입을 Provider에도 해주므로 환경을 고립시켜줄 수 있는 여건을 만들었다.
func testNotifyAuthChanged() {
    let notificationCenter = NotificationCenter()
    let poster = CurrentLocationProvider(notificationCenter: notificationCenter)
    let name = CurrentLocationProvider.authChangedNotification
    let expectation = XCTNSNotificationExpectation(name: name, object: poster, notificationCenter: notificationCenter)

    poster.notifyAuthChanged()
    wait(for: [expectation], timeout: 0)
}
  • 최종적으로 테스트코드를 다음과 같이 작성할 수 있다.

  • 예상값으로 XCTNSNotificationExpectation을 만드므로 해당 notificationCentername, object, notificationCenter까지 정확히 post되는지 여부를 확인할 수 있다.

  • 또한, wait(for: [expectation], timeout: 0)을 보면 timeout이 0초인것을 학인할 수 있는데 이는 .notifyAuthChanged의 메소드가 동작하는 순간 곧바로 expectation이 실행되므로 0초가 가능한 것이다.

  • ~19:49

반응형