iOS 10通知单元测试

18
在我的应用程序中,我希望断言通知已经以正确的格式添加。我通常会使用依赖注入来实现这一点,但我想不出一种方法来测试新的 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没有可以访问的初始化器,我无法实例化模拟对象。

我甚至不确定是否可以通过添加通知请求和获取当前通知来进行测试,因为测试需要在模拟器上请求权限,这将使测试停顿。目前,我已经将通知逻辑重构为包装器,因此我可以在应用程序中模拟它并手动测试。

除了手动测试,我还有其他更好的选择吗?

4个回答

27
你可以为你正在使用的方法创建一个协议,并在UNUserNotificationCenter上进行扩展以符合它。该协议将充当原始UNUserNotificationCenter实现和你的模拟对象之间的“桥梁”,以替换其方法实现。
这是我在playground中编写的示例代码,可以正常工作:
/* 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

请记住,每次使用新的方法时,您都需要将其添加到协议中,否则编译器会抱怨。

希望这有所帮助!


1
这是一个很好的回答。您认为是否可能采用类似的方法来模拟 func getNotificationSettings(completionHandler: @escaping (UNNotificationSettings) -> Swift.Void)?我在模拟返回的 UNNotificationSettings 对象时遇到了麻烦,因为它无法被实例化。 - JimmyB
这是一个好的测试吗?从上面的代码中,我可以看到你的TestClass有它自己的doSomething(),这意味着它永远不会调用实际的ExampleClass的doSomething()。这也意味着你可以在TestClass的doSomething()中写任何东西并使其通过。 - Harshad Kale
1
@JimmyB,是的,你仍然可以这样做。你可以创建虚假的UNNotificationSettings,但需要进行更多步骤。你不能实例化UNNotificationsSettings,因为它需要一个NSCoder,所以你还必须模拟NSCoder :p 你可以通过子类化NSCoder并覆盖所需方法'allowsKeycoding'、'decodeInt64'、'decodeObject'、'decodeBool'来实现这一点。一旦你有了这个,将其传递给MockUNNotification设置的init,你就完成了。你会遇到与UNNotification相同的问题,所以你可以传递相同的MockNSCoder。 - franciscojma86
@kalehv 这很好,因为我们不是在测试 UNNotificationCenter 是否有效,而是要确保它被调用。所以在这种情况下,如果你创建了一个 XCTestException,你可以在 doSomething 中调用 .fulfill()。希望这样说得清楚。 - franciscojma86
你说得对,这个测试有点多余了,而且我实际上并没有把它做成一个测试。现在应该很清楚,我们正在测试ExampleClass,同时模拟UNNotificationCenter并将其注入到ExampleClass中。感谢您指出这一点! - franciscojma86
显示剩余2条评论

3
你可以利用UNUserNotificationCenter,然后对返回的settings调用setValue
UNUserNotificationCenter.current().getNotificationSettings(completionHandler: { settings in
    let status: UNAuthorizationStatus = .authorized
    settings.setValue(status.rawValue, forKey: "authorizationStatus")
    completionHandler(settings)
})

在单元测试环境中,UNUserNotificationCenter.current() == nil 不是吗?对我来说是这样的。 - undefined

2

我使用以下方法成功模拟了UNNotificationSettings

let object = UIView()
let data = try! NSKeyedArchiver.archivedData(withRootObject: object, requiringSecureCoding: false)
let coder = try! NSKeyedUnarchiver(forReadingFrom: data)
let setting = UNNotificationSettings(coder: coder)
setting?.setValue(UNAuthorizationStatus.notDetermined.rawValue, forKey: "authorizationStatus")

1
我认为你找到了类似于我最近解决方案的东西:您可以使用class_createInstance(UNNotification.classForKeyedArchiver()...)并将值转换为UNNotification如果您想操作其内容和日期成员,则可以子类化UNNotification并使用相同的公式“更改类名和转换”来创建它,然后覆盖这些成员-这些成员是开放的-并返回您想要的任何内容。 - Bruno Garelli

0

虽然测试UNUserNotificationCenter是否被调用而不是它实际上是否起作用可能是正确的(Apple应该测试),但你不需要任何权限来安排并检查已安排的通知。权限只需要在实际显示通知时才需要(在单元测试中绝对不会测试这一点)。

在我的单元测试中,我调用真正的UNUserNotificationCenter实现,然后检查已安排的通知(UNUserNotificationCenter.current().getPendingNotificationRequests),所有这些都不需要任何权限,测试运行非常快。这种方法比已经提出的方法快得多(因为你需要编写更少的代码来进行测试)。


1
为什么要踩我?我甚至没有提倡调用真实服务的方法,我只是回应了问题作者所说的:“测试需要在模拟器上请求权限,这会使测试停滞不前”。我澄清了这并不是事实,您无需任何权限即可检查计划通知。 - NeverwinterMoon
1
您实际上需要权限将请求附加到待处理通知队列中。请再次检查。 - Bruno Garelli
不仅如此,我还使用这种方法在不同设备上运行自动化测试已经有一段时间了。但是这篇文章已经发布了3年,新的iOS版本可能会改变要求。现在我正在从事完全不同的项目,不再做任何相关工作。如果我的话不足以信服你,请查看:https://dev59.com/rGQo5IYBdhLWcg3wdPAa#25619622,https://dev59.com/rGQo5IYBdhLWcg3wdPAa#16598226。权限(也许曾经)只需要用于警报(显示/声音)通知。 - NeverwinterMoon

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接