Skip to content

Commit 75f2daf

Browse files
committed
Add more complete unit tests
1 parent 45b8a27 commit 75f2daf

File tree

1 file changed

+203
-13
lines changed

1 file changed

+203
-13
lines changed
Lines changed: 203 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,36 +1,226 @@
11
// Copyright 2024 Google LLC
22
//
3-
// Licensed under the Apache-Version 2.0 (the "License");
3+
// Licensed under the Apache License, Version 2.0 (the "License");
44
// you may not use this file except in compliance with the License.
55
// You may obtain a copy of the License at
66
//
77
// http://www.apache.org/licenses/LICENSE-2.0
88
//
9-
// Unless required by applicable law or agreed to in writing-software
10-
// distributed under the License is distributed on an "AS IS" BASIS-
11-
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND-either express or implied.
9+
// Unless required by applicable law or agreed to in writing, software
10+
// distributed under the License is distributed on an "AS IS" BASIS,
11+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
1212
// See the License for the specific language governing permissions and
1313
// limitations under the License.
1414

15+
import XCTest
1516
import FirebaseCore
1617
@testable import FirebaseRemoteConfig
1718

18-
import XCTest
19+
#if SWIFT_PACKAGE
20+
import RemoteConfigFakeConsoleObjC
21+
#endif
22+
23+
// MARK: - Mock Objects for Testing
24+
25+
/// A mock listener registration that allows tests to verify that its `remove()` method was called.
26+
class MockListenerRegistration: ConfigUpdateListenerRegistration, @unchecked Sendable {
27+
var wasRemoveCalled = false
28+
override func remove() {
29+
wasRemoveCalled = true
30+
}
31+
}
32+
33+
/// A mock for the RCNConfigRealtime component that allows tests to control the config update listener.
34+
class MockRealtime: RCNConfigRealtime {
35+
/// The listener closure captured from the `updates` async stream.
36+
var listener: ((RemoteConfigUpdate?, Error?) -> Void)?
37+
let mockRegistration = MockListenerRegistration()
38+
39+
40+
override func addConfigUpdateListener(
41+
_ listener: @escaping (RemoteConfigUpdate?, Error?) -> Void
42+
) -> ConfigUpdateListenerRegistration {
43+
self.listener = listener
44+
return mockRegistration
45+
}
46+
47+
/// Simulates the backend sending a successful configuration update.
48+
func sendUpdate(keys: [String]) {
49+
let update = RemoteConfigUpdate(updatedKeys: Set(keys))
50+
listener?(update, nil)
51+
}
52+
53+
/// Simulates the backend sending an error.
54+
func sendError(_ error: Error) {
55+
listener?(nil, error)
56+
}
57+
58+
/// Simulates the listener completing without an update or error.
59+
func sendCompletion() {
60+
listener?(nil, nil)
61+
}
62+
}
63+
64+
// MARK: - AsyncStreamTests2
1965

2066
@available(iOS 13, tvOS 13, macOS 10.15, macCatalyst 13, watchOS 7, *)
21-
class AsyncStreamTests: APITestBase {
22-
func testConfigUpdateStreamReceivesUpdates() async throws {
23-
guard APITests.useFakeConfig else { return }
24-
25-
let expectation = self.expectation(description: #function)
67+
class AsyncStreamTests: XCTestCase {
68+
var app: FirebaseApp!
69+
var config: RemoteConfig!
70+
var mockRealtime: MockRealtime!
2671

27-
Task {
72+
struct TestError: Error, Equatable {}
73+
74+
override func setUpWithError() throws {
75+
try super.setUpWithError()
76+
77+
// Perform one-time setup of the FirebaseApp for testing.
78+
if FirebaseApp.app() == nil {
79+
let options = FirebaseOptions(googleAppID: "1:123:ios:123abc",
80+
gcmSenderID: "correct_gcm_sender_id")
81+
options.apiKey = "A23456789012345678901234567890123456789"
82+
options.projectID = "Fake_Project"
83+
FirebaseApp.configure(options: options)
84+
}
85+
86+
app = FirebaseApp.app()!
87+
config = RemoteConfig.remoteConfig(app: app)
88+
89+
// Install the mock realtime service.
90+
mockRealtime = MockRealtime()
91+
config.configRealtime = mockRealtime
92+
}
93+
94+
override func tearDownWithError() throws {
95+
app = nil
96+
config = nil
97+
mockRealtime = nil
98+
try super.tearDownWithError()
99+
}
100+
101+
func testStreamYieldsUpdate_whenUpdateIsSent() async throws {
102+
let expectation = self.expectation(description: "Stream should yield an update.")
103+
let keysToUpdate = ["foo", "bar"]
104+
105+
let listeningTask = Task {
28106
for try await update in config.updates {
107+
XCTAssertEqual(update.updatedKeys, Set(keysToUpdate))
29108
expectation.fulfill()
109+
break // End the loop after receiving the expected update.
30110
}
31111
}
32112

33-
fakeConsole.config[Constants.key1] = Constants.value1
34-
await fulfillment(of: [expectation], timeout: 5)
113+
// Ensure the listener is attached before sending the update.
114+
try await Task.sleep(nanoseconds: 100_000_000) // 0.1 seconds
115+
116+
mockRealtime.sendUpdate(keys: keysToUpdate)
117+
118+
await fulfillment(of: [expectation], timeout: 1.0)
119+
listeningTask.cancel()
120+
}
121+
122+
func testStreamFinishes_whenErrorIsSent() async throws {
123+
let expectation = self.expectation(description: "Stream should throw an error.")
124+
let testError = TestError()
125+
126+
let listeningTask = Task {
127+
do {
128+
for try await _ in config.updates {
129+
XCTFail("Stream should not have yielded any updates.")
130+
}
131+
} catch {
132+
XCTAssertEqual(error as? TestError, testError)
133+
expectation.fulfill()
134+
}
135+
}
136+
137+
// Ensure the listener is attached before sending the error.
138+
try await Task.sleep(nanoseconds: 100_000_000) // 0.1 seconds
139+
140+
mockRealtime.sendError(testError)
141+
142+
await fulfillment(of: [expectation], timeout: 1.0)
143+
listeningTask.cancel()
144+
}
145+
146+
func testStreamCancellation_callsRemoveOnListener() async throws {
147+
let listeningTask = Task {
148+
for try await _ in config.updates {
149+
// We will cancel the task, so it should not reach here.
150+
}
151+
}
152+
153+
// Ensure the listener has time to be established.
154+
try await Task.sleep(nanoseconds: 100_000_000) // 0.1 seconds
155+
156+
// Verify the listener has not been removed yet.
157+
XCTAssertFalse(mockRealtime.mockRegistration.wasRemoveCalled)
158+
159+
// Cancel the task, which should trigger the stream's onTermination handler.
160+
listeningTask.cancel()
161+
162+
// Give the cancellation a moment to propagate.
163+
try await Task.sleep(nanoseconds: 100_000_000) // 0.1 seconds
164+
165+
// Verify the listener was removed.
166+
XCTAssertTrue(mockRealtime.mockRegistration.wasRemoveCalled)
167+
}
168+
169+
func testStreamFinishesGracefully_whenListenerSendsNil() async throws {
170+
let expectation = self.expectation(description: "Stream should finish without error.")
171+
172+
let listeningTask = Task {
173+
var updateCount = 0
174+
do {
175+
for try await _ in config.updates {
176+
updateCount += 1
177+
}
178+
// The loop finished without throwing, which is the success condition.
179+
XCTAssertEqual(updateCount, 0, "No updates should have been received.")
180+
expectation.fulfill()
181+
} catch {
182+
XCTFail("Stream should not have thrown an error, but threw \(error).")
183+
}
184+
}
185+
186+
try await Task.sleep(nanoseconds: 100_000_000)
187+
mockRealtime.sendCompletion()
188+
189+
await fulfillment(of: [expectation], timeout: 1.0)
190+
listeningTask.cancel()
191+
}
192+
193+
func testStreamYieldsMultipleUpdates_whenMultipleUpdatesAreSent() async throws {
194+
let expectation = self.expectation(description: "Stream should receive two updates.")
195+
expectation.expectedFulfillmentCount = 2
196+
197+
let updatesToSend = [
198+
Set(["key1", "key2"]),
199+
Set(["key3"]),
200+
]
201+
var receivedUpdates: [Set<String>] = []
202+
203+
let listeningTask = Task {
204+
for try await update in config.updates {
205+
receivedUpdates.append(update.updatedKeys)
206+
expectation.fulfill()
207+
if receivedUpdates.count == updatesToSend.count {
208+
break
209+
}
210+
}
211+
return receivedUpdates
212+
}
213+
214+
try await Task.sleep(nanoseconds: 100_000_000)
215+
216+
mockRealtime.sendUpdate(keys: Array(updatesToSend[0]))
217+
try await Task.sleep(nanoseconds: 100_000_000) // Brief pause between sends
218+
mockRealtime.sendUpdate(keys: Array(updatesToSend[1]))
219+
220+
await fulfillment(of: [expectation], timeout: 2.0)
221+
222+
let finalUpdates = try await listeningTask.value
223+
XCTAssertEqual(finalUpdates, updatesToSend)
224+
listeningTask.cancel()
35225
}
36226
}

0 commit comments

Comments
 (0)