【问题标题】:Unit testing iOS 10 notifications单元测试 iOS 10 通知
【发布时间】:2017-04-13 06:03:38
【问题描述】:

在我的应用程序中,我希望断言通知已以正确的格式添加。我通常会通过依赖注入来做到这一点,但我想不出一种方法来测试新的UNUserNotificationCenter API。

我开始创建一个模拟对象来捕获通知请求:

import Foundation
import UserNotifications

class NotificationCenterMock: UNUserNotificationCenter {
    var request: UNNotificationRequest? = nil
    override func add(_ request: UNNotificationRequest, withCompletionHandler completionHandler: ((Error?) -> Void)? = nil) {
        self.request = request
    }
}

但是,UNUserNotificationCenter 没有可访问的初始化程序,我无法实例化模拟。

我什至不确定是否可以通过添加通知请求并获取当前通知来进行测试,因为测试需要请求模拟器的权限,这会导致测试停止。目前我已经将通知逻辑重构为一个包装器,因此我至少可以在整个应用程序中模拟它并手动测试。

我有比手动测试更好的选择吗?

【问题讨论】:

    标签: ios unit-testing notifications ios10


    【解决方案1】:

    您可以使用UNUserNotificationCenter,然后在返回的settings 上使用setValue

    UNUserNotificationCenter.current().getNotificationSettings(completionHandler: { settings in
        let status: UNAuthorizationStatus = .authorized
        settings.setValue(status.rawValue, forKey: "authorizationStatus")
        completionHandler(settings)
    })
    

    【讨论】:

      【解决方案2】:

      虽然测试UNUserNotificationCenter 是否被调用而不是测试它是否实际工作(Apple 应该测试它)很可能是正确的,但您不需要任何权限来安排然后检查预定通知。只有在实际显示通知时才需要权限(而且您绝对不会在单元测试中对其进行测试)。

      在我的单元测试中,我调用真正的UNUserNotificationCenter 实现,然后检查预定的通知 (UNUserNotificationCenter.current().getPendingNotificationRequests),所有这些都无需任何权限即可工作,并且测试运行得非常快。这种方法比已经提出的方法快得多(从这个意义上说,您需要编写更少的代码来进行测试)。

      【讨论】:

      • 为什么投反对票?我什至不提倡调用真实服务的方法,我只是对问题作者所说的做出了反应:“测试需要在模拟器上请求许可,这会停止测试”。我澄清说事实并非如此,您不需要任何权限即可查看预定通知。
      • 您实际上需要权限才能将请求附加到待处理的通知队列。再次检查
      • 我不仅检查了这一点,而且有一段时间我使用这种方法在不同的设备上运行了自动化测试。但这已在 3 年前发布,可能是较新的 iOS 版本更改了要求。现在我正在做一个完全不同的项目,但没有做任何事情。不过,如果我的话还不够,请检查:stackoverflow.com/a/25619622/1719285stackoverflow.com/a/16598226/1719285。权限(可能是)只需要提醒(显示/声音)通知。
      【解决方案3】:

      您可以为您正在使用的方法创建一个协议,并在 UNUserNotificationCenter 上进行扩展以符合它。 该协议将充当原始 UNUserNotificationCenter 实现和您的模拟对象之间的“桥梁”,以替换其方法实现。

      这是我在操场上编写的示例代码,运行良好:

      /* UNUserNotificationCenterProtocol.swift */
      
      // This protocol allows you to use UNUserNotificationCenter, and replace the implementation of its 
      // methods in you test classes.
      protocol UNUserNotificationCenterProtocol: class {
        // Declare only the methods that you'll be using.
        func add(_ request: UNNotificationRequest,
                 withCompletionHandler completionHandler: ((Error?) -> Void)?)
      }
      
      // The mock class that you'll be using for your test classes. Replace the method contents with your mock
      // objects.
      class MockNotificationCenter: UNUserNotificationCenterProtocol {
      
        var addRequestExpectation: XCTestExpectation?
      
        func add(_ request: UNNotificationRequest,
                 withCompletionHandler completionHandler: ((Error?) -> Void)?) {
          // Do anything you want here for your tests, fulfill the expectation to pass the test.
          addRequestExpectation?.fulfill()
          print("Mock center log")
          completionHandler?(nil)
        }
      }
      
      // Must extend UNUserNotificationCenter to conform to this protocol in order to use it in your class.
      extension UNUserNotificationCenter: UNUserNotificationCenterProtocol {
      // I'm only adding this implementation to show a log message in this example. In order to use the original implementation, don't add it here.
        func add(_ request: UNNotificationRequest, withCompletionHandler completionHandler: ((Error?) -> Void)?) {
          print("Notification center log")
          completionHandler?(nil)
        }
      }
      
      /* ExampleClass.swift */
      
      class ExampleClass {
      
        // Even though the type is UNUserNotificationCenterProtocol, it will take UNUserNotificationCenter type
        // because of the extension above.
        var notificationCenter: UNUserNotificationCenterProtocol = UNUserNotificationCenter.current()
      
        func doSomething() {
          // Create a request.
          let content = UNNotificationContent()
          let request = UNNotificationRequest(identifier: "Request",
                                                 content: content,
                                                 trigger: nil)
          notificationCenter.add(request) { (error: Error?) in
            // completion handler code
          }
        }
      }
      
      let exampleClass = ExampleClass()
      exampleClass.doSomething() // This should log "Notification center log"
      
      EDITED:
      /* TestClass.Swift (unit test class) */
      
      class TestClass {
        // Class being tested 
        var exampleClass: ExampleClass!    
        // Create your mock class.
        var mockNotificationCenter = MockNotificationCenter()
      
        func setUp() {
           super.setUp()
           exampleClass = ExampleClass()
           exampleClass.notificationCenter = mockNotificationCenter 
        }
      
        func testDoSomething() {
          mockNotificationCenter.addRequestExpectation = expectation(description: "Add request should've been called")
          exampleClass.doSomething()
          waitForExpectations(timeout: 1)
        }
      }
      // Once you run the test, the expectation will be called and "Mock Center Log" will be printed
      

      请记住,每次使用新方法时,都必须将其添加到协议中,否则编译器会报错。

      希望这会有所帮助!

      【讨论】:

      • 这是一个很好的回应。你认为有可能采用类似的方法来模拟func getNotificationSettings(completionHandler: @escaping (UNNotificationSettings) -> Swift.Void)吗?我无法模拟返回的 UNNotificationSettings 对象,因为它无法实例化。
      • 这是一个很好的测试吗?从上面的代码中,我可以看到您的 TestClass 有自己的 doSomething(),这意味着它永远不会调用实际的 ExampleClass 的 doSomething()。这也意味着您可以在 TestClass 的 doSomething() 中编写任何内容并使其通过
      • @JimmyB 是的,你仍然可以这样做。您可以创建假的 UNNotificationSettings,但它需要几个步骤。您不能实例化 UNNotificationsSettings,因为它需要 NSCoder,因此您还必须模拟 NSCoder:p 您可以通过子类化 NSCoder 并覆盖所需的方法来做到这一点:'allowsKeycoding'、'decodeInt64'、'decodeObject'、'解码布尔'。一旦你有了它,将它传递给 MockUNNotification 设置的 init ,你就完成了。 UNNotification 也会有同样的问题,所以你可以传递相同的 MockNSCoder。
      • @kalehv 这很好,因为我们没有测试 UNNotificationCenter 是否有效,我们只是想确保它被调用。所以在这种情况下,如果你创建了一个 XCTestException,你可以在 doSomething 中调用 .fulfill()。希望这是有道理的。
      • @franciscojma86 如果我错了请纠正我,但我的印象是您的TestClass 正在尝试测试ExampleClass 是否确实调用add。如果是这种情况,那么您的 TestClass 没有进行测试。
      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2023-04-04
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多