From ba867503c6753bfb920541139b01963e99c1a327 Mon Sep 17 00:00:00 2001 From: Kaushal Kapasi Date: Tue, 9 Apr 2024 12:09:33 -0400 Subject: [PATCH] chore: add tests for batched event sending --- DevCycle/Networking/DevCycleService.swift | 1 + DevCycleTests/Mocks/URLSessionMock.swift | 1 + .../Models/DevCycleClientTests.swift | 7 +- DevCycleTests/Models/EventQueueTests.swift | 3 +- .../Networking/DevCycleServiceTests.swift | 140 +++++++++++++++++- 5 files changed, 146 insertions(+), 6 deletions(-) diff --git a/DevCycle/Networking/DevCycleService.swift b/DevCycle/Networking/DevCycleService.swift index 213ef0d..99805ef 100644 --- a/DevCycle/Networking/DevCycleService.swift +++ b/DevCycle/Networking/DevCycleService.swift @@ -94,6 +94,7 @@ class DevCycleService: DevCycleServiceProtocol { } func publishEvents(events: [DevCycleEvent], user: DevCycleUser, completion: @escaping PublishEventsCompletionHandler) { + print("in real publishEvents") var eventsRequest = createEventsRequest() let userEncoder = JSONEncoder() userEncoder.dateEncodingStrategy = .iso8601 diff --git a/DevCycleTests/Mocks/URLSessionMock.swift b/DevCycleTests/Mocks/URLSessionMock.swift index 6d736e6..47b5325 100644 --- a/DevCycleTests/Mocks/URLSessionMock.swift +++ b/DevCycleTests/Mocks/URLSessionMock.swift @@ -29,6 +29,7 @@ class URLSessionMock: URLSession { let data = self.data let error = self.error return URLSessionDataTaskMock { + print("in completion handler for URL Session Mock") completionHandler(data, nil, error) } } diff --git a/DevCycleTests/Models/DevCycleClientTests.swift b/DevCycleTests/Models/DevCycleClientTests.swift index 8d4708c..7a9a59c 100644 --- a/DevCycleTests/Models/DevCycleClientTests.swift +++ b/DevCycleTests/Models/DevCycleClientTests.swift @@ -252,7 +252,6 @@ class DevCycleClientTest: XCTestCase { let variableValue = client.variableValue(key: "some_non_existent_variable", defaultValue: false) XCTAssertFalse(variableValue) - client.close(callback: nil) } func testVariableStringDefaultValue() { @@ -318,12 +317,13 @@ class DevCycleClientTest: XCTestCase { let nsDicDefault: NSDictionary = ["key":"val"] let variable2 = client.variable(key: "some_non_existent_variable", defaultValue: nsDicDefault) XCTAssertEqual(variable2.defaultValue, nsDicDefault) - XCTAssertEqual(variable2.type, DVCVariableTypes.JSON) + XCTAssertEqual(variable2.type, DVCVariableTypes.JSON) } func testVariableMethodReturnsCorrectVariableForKey() { let client = try! self.builder.user(self.user).sdkKey("my_sdk_key").build(onInitialized: nil) client.initialize(callback: nil) + client.setup(service: self.service) client.config?.userConfig = self.userConfig let boolVar = client.variable(key: "bool-var", defaultValue: false) @@ -354,6 +354,7 @@ class DevCycleClientTest: XCTestCase { func testVariableMethodReturnSameInstanceOfVariable() { let client = try! self.builder.user(self.user).sdkKey("my_sdk_key").build(onInitialized: nil) client.initialize(callback: nil) + client.setup(service: self.service) client.config?.userConfig = self.userConfig let boolVar = client.variable(key: "bool-var", defaultValue: false) @@ -374,6 +375,7 @@ class DevCycleClientTest: XCTestCase { func testVariableMethodReturnsDifferentVariableForANewDefaultValue() { let client = try! self.builder.user(self.user).sdkKey("my_sdk_key").build(onInitialized: nil) client.initialize(callback: nil) + client.setup(service: self.service) client.config?.userConfig = self.userConfig var stringVar = client.variable(key: "string-var", defaultValue: "default value") @@ -583,6 +585,7 @@ extension DevCycleClientTest { } func publishEvents(events: [DevCycleEvent], user: DevCycleUser, completion: @escaping PublishEventsCompletionHandler) { + print("in mocked DevCycle Client Tests publishEvents") self.publishCallCount += 1 self.eventPublishCount += events.count XCTAssert(true) diff --git a/DevCycleTests/Models/EventQueueTests.swift b/DevCycleTests/Models/EventQueueTests.swift index 5aeaabe..f6beb1e 100644 --- a/DevCycleTests/Models/EventQueueTests.swift +++ b/DevCycleTests/Models/EventQueueTests.swift @@ -82,7 +82,6 @@ private class MockService: DevCycleServiceProtocol { func getConfig(user: DevCycleUser, enableEdgeDB: Bool, extraParams: RequestParams?, completion: @escaping ConfigCompletionHandler) {} func publishEvents(events: [DevCycleEvent], user: DevCycleUser, completion: @escaping PublishEventsCompletionHandler) { - DispatchQueue.main.asyncAfter(deadline: .now() + 1.0) { completion((nil, nil, nil)) } @@ -101,7 +100,7 @@ class MockWithErrorCodeService: DevCycleServiceProtocol { func getConfig(user: DevCycleUser, enableEdgeDB: Bool, extraParams: RequestParams?, completion: @escaping ConfigCompletionHandler) {} func publishEvents(events: [DevCycleEvent], user: DevCycleUser, completion: @escaping PublishEventsCompletionHandler) { - let error = NSError(domain: "api.devcycle.com", code: self.errorCode) + let error = NSError(domain: "devcycle.com", code: self.errorCode) DispatchQueue.main.asyncAfter(deadline: .now() + 1.0) { completion((nil, nil, error)) } diff --git a/DevCycleTests/Networking/DevCycleServiceTests.swift b/DevCycleTests/Networking/DevCycleServiceTests.swift index 12abeaa..446d934 100644 --- a/DevCycleTests/Networking/DevCycleServiceTests.swift +++ b/DevCycleTests/Networking/DevCycleServiceTests.swift @@ -72,11 +72,50 @@ class DevCycleServiceTests: XCTestCase { } func testProcessConfigReturnsNilIfBrokenJson() throws { - let service = getService() let data = "{\"config\":\"key}".data(using: .utf8) let config = processConfig(data) XCTAssertNil(config) } + + func testFlushingEvents() { + let service = MockDevCycleService() + let eventQueue = EventQueue() + let user = try! DevCycleUser.builder().userId("user1").build() + let expectation = XCTestExpectation(description: "Events are flushed in a single batch") + + // Generate 205 custom events and add them to the queue + for i in 0..<10 { + let event = try! DevCycleEvent.builder().type("event_\(i)").build() + eventQueue.queue(event) + } + eventQueue.flush(service: service, user: user, callback: nil) + DispatchQueue.main.asyncAfter(deadline: .now() + 1.0) { + XCTAssertEqual(eventQueue.events.count, 0) + expectation.fulfill() + } + wait(for: [expectation], timeout: 1.0) + XCTAssertEqual(service.makeRequestCallCount, 1, "makeRequest should have been called 1 time") + } + + func testFlushingEventsInBatches() { + let service = MockDevCycleService() + let eventQueue = EventQueue() + let user = try! DevCycleUser.builder().userId("user1").build() + let expectation = XCTestExpectation(description: "Events are serially queued and flushed in multiple batches") + + // Generate 205 custom events and add them to the queue + for i in 0..<205 { + let event = try! DevCycleEvent.builder().type("event_\(i)").build() + eventQueue.queue(event) + } + eventQueue.flush(service: service, user: user, callback: nil) + DispatchQueue.main.asyncAfter(deadline: .now() + 1.0) { + XCTAssertEqual(eventQueue.events.count, 0) + expectation.fulfill() + } + wait(for: [expectation], timeout: 3.0) + XCTAssertEqual(service.makeRequestCallCount, 3, "makeRequest should have been called 3 times") + } } extension DevCycleServiceTests { @@ -111,6 +150,104 @@ extension DevCycleServiceTests { } } + class MockDevCycleService: DevCycleServiceProtocol { + func getConfig(user: DevCycle.DevCycleUser, enableEdgeDB: Bool, extraParams: DevCycle.RequestParams?, completion: @escaping DevCycle.ConfigCompletionHandler) { + // Empty Stub + } + + func saveEntity(user: DevCycle.DevCycleUser, completion: @escaping DevCycle.SaveEntityCompletionHandler) { + // Empty Stub + } + + var publishEventsCalled = false + var makeRequestCallCount = 0 + let testMaxBatchSize = 100 + + func publishEvents(events: [DevCycleEvent], user: DevCycleUser, completion: @escaping PublishEventsCompletionHandler) { + print("in mocked DevCycleServiceTests publishEvents") + publishEventsCalled = true + + let url = URL(string: "http://test.com/v1/events")! + var eventsRequest = URLRequest(url: url) + let userEncoder = JSONEncoder() + userEncoder.dateEncodingStrategy = .iso8601 + guard let userId = user.userId, let userData = try? userEncoder.encode(user) else { + return completion((nil, nil, ClientError.MissingUser)) + } + + let eventPayload = self.generateEventPayload(events, userId, nil) + guard let userBody = try? JSONSerialization.jsonObject(with: userData, options: .fragmentsAllowed) else { + return completion((nil, nil, ClientError.InvalidUser)) + } + + let totalEventsCount = eventPayload.count + var startIndex = 0 + var endIndex = min(self.testMaxBatchSize, totalEventsCount) + + while startIndex < totalEventsCount { + let batchEvents = Array(eventPayload[startIndex..= totalEventsCount { + return completion((data, response, nil)) + } + } + } + } + + func makeRequest(request: URLRequest, completion: @escaping CompletionHandler) { + self.makeRequestCallCount += 1 + + // Mock implementation for makeRequest + let mockData = "Successfully flushed 100 events".data(using: .utf8) + let mockResponse = HTTPURLResponse(url: URL(string: "https://example.com")!, statusCode: 200, httpVersion: "HTTP/1.1", headerFields: nil) + completion((mockData, mockResponse, nil)) + } + + private func generateEventPayload(_ events: [DevCycleEvent], _ userId: String, _ featureVariables: [String:String]?) -> [[String:Any]] { + var eventsJSON: [[String:Any]] = [] + let formatter = ISO8601DateFormatter() + + for event in events { + if event.type == nil { + continue + } + let eventDate: Date = event.clientDate ?? Date() + var eventToPost: [String: Any] = [ + "type": event.type!, + "clientDate": formatter.string(from: eventDate), + "user_id": userId, + "featureVars": featureVariables ?? [:] + ] + + if (event.target != nil) { eventToPost["target"] = event.target } + if (event.value != nil) { eventToPost["value"] = event.value } + if (event.metaData != nil) { eventToPost["metaData"] = event.metaData } + if (event.type != "variableDefaulted" && event.type != "variableEvaluated") { + eventToPost["customType"] = event.type + eventToPost["type"] = "customEvent" + } + + eventsJSON.append(eventToPost) + } + + return eventsJSON + } + } + + func getService(_ options: DevCycleOptions? = nil) -> DevCycleService { let user = getTestUser() let config = DVCConfig(sdkKey: "my_sdk_key", user: user) @@ -122,7 +259,6 @@ extension DevCycleServiceTests { .userId("my_user") .build() } - }