Attempt to improve unit test reliability:

- synchronize mock properties on the main queue
- sleep for longer when processing routes
- finish encoding before deallocating the keyed archiver
- wait for NotificationCenter notifications to be delivered
- change were we process remote notification permissions
This commit is contained in:
Stefan Ceriu 2024-04-10 16:29:30 +03:00 committed by Stefan Ceriu
parent da598302df
commit 7645971812
6 changed files with 8825 additions and 378 deletions

File diff suppressed because it is too large Load Diff

View File

@ -23,17 +23,7 @@ final class NotificationManager: NSObject, NotificationManagerProtocol {
private let notificationCenter: UserNotificationCenterProtocol
private let appSettings: AppSettings
private var userSession: UserSessionProtocol? {
didSet {
// If notification permissions were given previously then attempt re-registering
// for remote notifications on startup. Otherwise let the onboarding flow handle it
Task { @MainActor in
if await self.notificationCenter.authorizationStatus() == .authorized {
self.delegate?.registerForRemoteNotifications()
}
}
}
}
private var userSession: UserSessionProtocol?
private var cancellables = Set<AnyCancellable>()
private var notificationsEnabled = false
@ -103,6 +93,18 @@ final class NotificationManager: NSObject, NotificationManagerProtocol {
func setUserSession(_ userSession: UserSessionProtocol?) {
self.userSession = userSession
// If notification permissions were given previously then attempt re-registering
// for remote notifications on startup. Otherwise let the onboarding flow handle it
Task { [weak self] in
guard let self else { return }
if await notificationCenter.authorizationStatus() == .authorized {
await MainActor.run {
delegate?.registerForRemoteNotifications()
}
}
}
}
func registrationFailed(with error: Error) {

View File

@ -45,7 +45,30 @@ import {{ import }}
{% call methodThrowableErrorDeclaration method %}
{% endif %}
{% if not method.isInitializer %}
{% call accessLevel method.accessLevel %}{% call staticSpecifier method %}var {% call swiftifyMethodName method.selectorName %}CallsCount = 0
var {% call swiftifyMethodName method.selectorName %}UnderlyingCallsCount = 0
{% call accessLevel method.accessLevel %}{% call staticSpecifier method %}var {% call swiftifyMethodName method.selectorName %}CallsCount: Int {
get {
if Thread.isMainThread {
return {% call swiftifyMethodName method.selectorName %}UnderlyingCallsCount
} else {
var returnValue: Int? = nil
DispatchQueue.main.sync {
returnValue = {% call swiftifyMethodName method.selectorName %}UnderlyingCallsCount
}
return returnValue!
}
}
set {
if Thread.isMainThread {
{% call swiftifyMethodName method.selectorName %}UnderlyingCallsCount = newValue
} else {
DispatchQueue.main.sync {
{% call swiftifyMethodName method.selectorName %}UnderlyingCallsCount = newValue
}
}
}
}
{% call accessLevel method.accessLevel %}{% call staticSpecifier method %}var {% call swiftifyMethodName method.selectorName %}Called: Bool {
return {% call swiftifyMethodName method.selectorName %}CallsCount > 0
}
@ -63,7 +86,31 @@ import {{ import }}
{% call accessLevel method.accessLevel %}{% call staticSpecifier method %}var {% call swiftifyMethodName method.selectorName %}ReceivedInvocations: [({% for param in method.parameters %}{{ param.name }}: {{ param.unwrappedTypeName if param.typeAttributes.escaping else param.typeName }}{{ ', ' if not forloop.last }}{% endfor %})] = []
{% endif %}
{% if not method.returnTypeName.isVoid and not method.isInitializer %}
{% call accessLevel method.accessLevel %}{% call staticSpecifier method %}var {% call swiftifyMethodName method.selectorName %}ReturnValue: {{ '(' if method.returnTypeName.isClosure and not method.isOptionalReturnType }}{{ method.returnTypeName }}{{ ')' if method.returnTypeName.isClosure and not method.isOptionalReturnType }}{{ '!' if not method.isOptionalReturnType }}
var {% call swiftifyMethodName method.selectorName %}UnderlyingReturnValue: {{ '(' if method.returnTypeName.isClosure and not method.isOptionalReturnType }}{{ method.returnTypeName }}{{ ')' if method.returnTypeName.isClosure and not method.isOptionalReturnType }}{{ '!' if not method.isOptionalReturnType }}
{% call accessLevel method.accessLevel %}{% call staticSpecifier method %}var {% call swiftifyMethodName method.selectorName %}ReturnValue: {{ '(' if method.returnTypeName.isClosure and not method.isOptionalReturnType }}{{ method.returnTypeName }}{{ ')' if method.returnTypeName.isClosure and not method.isOptionalReturnType }}{{ '!' if not method.isOptionalReturnType }} {
get {
if Thread.isMainThread {
return {% call swiftifyMethodName method.selectorName %}UnderlyingReturnValue
} else {
var returnValue: {{ method.returnTypeName }}? = nil
DispatchQueue.main.sync {
returnValue = {% call swiftifyMethodName method.selectorName %}UnderlyingReturnValue
}
return returnValue!
}
}
set {
if Thread.isMainThread {
{% call swiftifyMethodName method.selectorName %}UnderlyingReturnValue = newValue
} else {
DispatchQueue.main.sync {
{% call swiftifyMethodName method.selectorName %}UnderlyingReturnValue = newValue
}
}
}
}
{% endif %}
{% call methodClosureDeclaration method %}

View File

@ -17,6 +17,10 @@
import Foundation
final class MockCoder: NSKeyedArchiver {
deinit {
finishEncoding()
}
override func decodeObject(forKey _: String) -> Any { "" }
override func decodeInt64(forKey key: String) -> Int64 { 0 }
}

View File

@ -212,10 +212,14 @@ final class NotificationManagerTests: XCTestCase {
}
// No interaction if the object is nil or of the wrong type
NotificationCenter.default.post(name: .roomMarkedAsRead, object: nil)
try? await Task.sleep(for: .seconds(0.25)) // Wait for the notification to be delivered
XCTAssertEqual(notificationCenter.deliveredNotificationsCallsCount, 0)
XCTAssertEqual(notificationCenter.removeDeliveredNotificationsWithIdentifiersCallsCount, 0)
NotificationCenter.default.post(name: .roomMarkedAsRead, object: 1)
try? await Task.sleep(for: .seconds(0.25)) // Wait for the notification to be delivered
XCTAssertEqual(notificationCenter.deliveredNotificationsCallsCount, 0)
XCTAssertEqual(notificationCenter.removeDeliveredNotificationsWithIdentifiersCallsCount, 0)
@ -226,7 +230,10 @@ final class NotificationManagerTests: XCTestCase {
notificationCenter.removeDeliveredNotificationsWithIdentifiersClosure = { _ in
expectation.fulfill()
}
NotificationCenter.default.post(name: .roomMarkedAsRead, object: "RoomID")
try? await Task.sleep(for: .seconds(0.25)) // Wait for the notification to be delivered
await fulfillment(of: [expectation])
XCTAssertEqual(notificationCenter.deliveredNotificationsCallsCount, 1)
XCTAssertEqual(notificationCenter.removeDeliveredNotificationsWithIdentifiersCallsCount, 1)
@ -238,7 +245,10 @@ final class NotificationManagerTests: XCTestCase {
notificationCenter.removeDeliveredNotificationsWithIdentifiersClosure = { _ in
expectation.fulfill()
}
NotificationCenter.default.post(name: .invitesScreenAppeared, object: nil)
try? await Task.sleep(for: .seconds(0.25)) // Wait for the notification to be delivered
await fulfillment(of: [expectation])
XCTAssertEqual(notificationCenter.deliveredNotificationsCallsCount, 1)
XCTAssertEqual(notificationCenter.removeDeliveredNotificationsWithIdentifiersCallsCount, 1)

View File

@ -155,12 +155,8 @@ class RoomFlowCoordinatorTests: XCTestCase {
private func process(route: AppRoute) async throws {
roomFlowCoordinator.handleAppRoute(route, animated: true)
if case .childRoom = route {
// A single yield isn't enough when creating the new flow coordinator.
try await Task.sleep(for: .milliseconds(100))
} else {
await Task.yield()
}
// A single yield isn't enough when creating the new flow coordinator.
try await Task.sleep(for: .milliseconds(100))
}
private func process(route: AppRoute, expectedAction: RoomFlowCoordinatorAction) async throws {