Skip to content

Commit 9c9b9b0

Browse files
Update test cases
1 parent b40adda commit 9c9b9b0

File tree

2 files changed

+134
-130
lines changed

2 files changed

+134
-130
lines changed

Sources/CMAB/CmabClient.swift

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -19,16 +19,14 @@ import Foundation
1919
enum CmabClientError: Error, Equatable {
2020
case fetchFailed(String)
2121
case invalidResponse
22-
case decodingError
2322

2423
var message: String {
2524
switch self {
2625
case .fetchFailed(let message):
2726
return message
2827
case .invalidResponse:
2928
return "Invalid response from CMA-B server"
30-
case .decodingError:
31-
return "Error decoding CMA-B response"
29+
3230
}
3331
}
3432
}
@@ -177,7 +175,7 @@ class DefaultCmabClient: CmabClient {
177175
completion(.failure(CmabClientError.invalidResponse))
178176
}
179177
} catch {
180-
completion(.failure(CmabClientError.decodingError))
178+
completion(.failure(CmabClientError.invalidResponse))
181179
}
182180
}
183181
task.resume()

Tests/OptimizelyTests-Common/CMABClientTests.swift

Lines changed: 132 additions & 126 deletions
Original file line numberDiff line numberDiff line change
@@ -19,212 +19,218 @@ import XCTest
1919
class DefaultCmabClientTests: XCTestCase {
2020
var client: DefaultCmabClient!
2121
var mockSession: MockURLSession!
22+
var shortRetryConfig: CmabRetryConfig!
2223

2324
override func setUp() {
2425
super.setUp()
2526
mockSession = MockURLSession()
26-
client = DefaultCmabClient(session: mockSession)
27+
shortRetryConfig = CmabRetryConfig(maxRetries: 2, initialBackoff: 0.01, maxBackoff: 0.05, backoffMultiplier: 1.0)
28+
client = DefaultCmabClient(session: mockSession, retryConfig: shortRetryConfig)
2729
}
2830

2931
override func tearDown() {
3032
client = nil
3133
mockSession = nil
34+
shortRetryConfig = nil
3235
super.tearDown()
3336
}
3437

35-
func testFetchDecisionSuccess() {
36-
let expectedVariationId = "variation-123"
37-
let responseJSON: [String: Any] = [
38+
// MARK: - Helpers
39+
40+
func makeSuccessResponse(variationId: String) -> (Data, URLResponse, Error?) {
41+
let json: [String: Any] = [
3842
"predictions": [
39-
["variation_id": expectedVariationId]
43+
["variation_id": variationId]
4044
]
4145
]
42-
let responseData = try! JSONSerialization.data(withJSONObject: responseJSON, options: [])
43-
mockSession.nextData = responseData
44-
mockSession.nextResponse = HTTPURLResponse(url: URL(string: "https://prediction.cmab.optimizely.com/predict/abc")!,
45-
statusCode: 200, httpVersion: nil, headerFields: nil)
46-
mockSession.nextError = nil
46+
let data = try! JSONSerialization.data(withJSONObject: json, options: [])
47+
let response = HTTPURLResponse(url: URL(string: "https://prediction.cmab.optimizely.com/predict/abc")!,
48+
statusCode: 200, httpVersion: nil, headerFields: nil)!
49+
return (data, response, nil)
50+
}
51+
52+
func makeFailureResponse() -> (Data, URLResponse, Error?) {
53+
let response = HTTPURLResponse(url: URL(string: "https://prediction.cmab.optimizely.com/predict/abc")!,
54+
statusCode: 500, httpVersion: nil, headerFields: nil)!
55+
return (Data(), response, nil)
56+
}
57+
58+
// MARK: - Test Cases
59+
60+
func testFetchDecision_SuccessOnFirstTry() {
61+
let (successData, successResponse, _) = makeSuccessResponse(variationId: "variation-123")
62+
mockSession.responses = [(successData, successResponse, nil)]
4763

4864
let expectation = self.expectation(description: "Completion called")
49-
5065
client.fetchDecision(
51-
ruleId: "abc",
52-
userId: "user1",
53-
attributes: ["foo": "bar"],
54-
cmabUUID: "uuid"
66+
ruleId: "abc", userId: "user1",
67+
attributes: ["foo": "bar"], cmabUUID: "uuid"
5568
) { result in
56-
switch result {
57-
case .success(let variationId):
58-
XCTAssertEqual(variationId, expectedVariationId)
59-
case .failure(let error):
60-
XCTFail("Expected success, got failure: \(error)")
69+
if case let .success(variationId) = result {
70+
XCTAssertEqual(variationId, "variation-123")
71+
XCTAssertEqual(self.mockSession.callCount, 1)
72+
} else {
73+
XCTFail("Expected success result")
6174
}
6275
expectation.fulfill()
6376
}
64-
65-
waitForExpectations(timeout: 2, handler: nil)
77+
waitForExpectations(timeout: 1)
6678
}
6779

68-
func testFetchDecisionHttpError() {
69-
mockSession.nextData = Data()
70-
mockSession.nextResponse = HTTPURLResponse(url: URL(string: "https://prediction.cmab.optimizely.com/predict/abc")!,
71-
statusCode: 500, httpVersion: nil, headerFields: nil)
72-
mockSession.nextError = nil
80+
func testFetchDecision_SuccessOnSecondTry() {
81+
let (successData, successResponse, _) = makeSuccessResponse(variationId: "variation-retry")
82+
let fail = makeFailureResponse()
83+
mockSession.responses = [fail, (successData, successResponse, nil)]
7384

7485
let expectation = self.expectation(description: "Completion called")
75-
7686
client.fetchDecision(
77-
ruleId: "abc",
78-
userId: "user1",
79-
attributes: ["foo": "bar"],
80-
cmabUUID: "uuid"
87+
ruleId: "abc", userId: "user1",
88+
attributes: ["foo": "bar"], cmabUUID: "uuid"
8189
) { result in
82-
switch result {
83-
case .success(_):
84-
XCTFail("Expected failure, got success")
85-
case .failure(let error):
86-
XCTAssertTrue("\(error)".contains("HTTP error code"))
90+
if case let .success(variationId) = result {
91+
XCTAssertEqual(variationId, "variation-retry")
92+
XCTAssertEqual(self.mockSession.callCount, 2)
93+
} else {
94+
XCTFail("Expected success after retry")
8795
}
8896
expectation.fulfill()
8997
}
90-
91-
waitForExpectations(timeout: 2, handler: nil)
98+
waitForExpectations(timeout: 2)
9299
}
93100

94-
func testFetchDecisionInvalidJson() {
95-
mockSession.nextData = Data("not a json".utf8)
96-
mockSession.nextResponse = HTTPURLResponse(url: URL(string: "https://prediction.cmab.optimizely.com/predict/abc")!,
97-
statusCode: 200, httpVersion: nil, headerFields: nil)
98-
mockSession.nextError = nil
101+
func testFetchDecision_SuccessOnThirdTry() {
102+
let (successData, successResponse, _) = makeSuccessResponse(variationId: "success-third")
103+
let fail = makeFailureResponse()
104+
mockSession.responses = [fail, fail, (successData, successResponse, nil)]
99105

100106
let expectation = self.expectation(description: "Completion called")
101-
102107
client.fetchDecision(
103-
ruleId: "abc",
104-
userId: "user1",
105-
attributes: ["foo": "bar"],
106-
cmabUUID: "uuid"
108+
ruleId: "abc", userId: "user1",
109+
attributes: ["foo": "bar"], cmabUUID: "uuid"
107110
) { result in
108-
switch result {
109-
case .success(_):
110-
XCTFail("Expected failure, got success")
111-
case .failure(let error):
112-
XCTAssertTrue(error is CmabClientError)
111+
if case let .success(variationId) = result {
112+
XCTAssertEqual(variationId, "success-third")
113+
XCTAssertEqual(self.mockSession.callCount, 3)
114+
} else {
115+
XCTFail("Expected success after two retries")
113116
}
114117
expectation.fulfill()
115118
}
119+
waitForExpectations(timeout: 2)
120+
}
121+
122+
func testFetchDecision_ExhaustsAllRetries() {
123+
let fail = makeFailureResponse()
124+
mockSession.responses = [fail, fail, fail]
116125

117-
waitForExpectations(timeout: 2, handler: nil)
126+
let expectation = self.expectation(description: "Completion called")
127+
client.fetchDecision(
128+
ruleId: "abc", userId: "user1",
129+
attributes: ["foo": "bar"], cmabUUID: "uuid"
130+
) { result in
131+
if case let .failure(error) = result {
132+
XCTAssertTrue("\(error)".contains("Exhausted all retries"))
133+
XCTAssertEqual(self.mockSession.callCount, 3)
134+
} else {
135+
XCTFail("Expected failure after all retries")
136+
}
137+
expectation.fulfill()
138+
}
139+
waitForExpectations(timeout: 2)
118140
}
119141

120-
func testFetchDecisionInvalidResponseStructure() {
121-
let responseJSON: [String: Any] = [
122-
"not_predictions": []
142+
func testFetchDecision_HttpError() {
143+
mockSession.responses = [
144+
(Data(), HTTPURLResponse(url: URL(string: "https://prediction.cmab.optimizely.com/predict/abc")!,
145+
statusCode: 500, httpVersion: nil, headerFields: nil), nil)
123146
]
124-
let responseData = try! JSONSerialization.data(withJSONObject: responseJSON, options: [])
125-
mockSession.nextData = responseData
126-
mockSession.nextResponse = HTTPURLResponse(url: URL(string: "https://prediction.cmab.optimizely.com/predict/abc")!,
127-
statusCode: 200, httpVersion: nil, headerFields: nil)
128-
mockSession.nextError = nil
129147

130148
let expectation = self.expectation(description: "Completion called")
131-
132149
client.fetchDecision(
133-
ruleId: "abc",
134-
userId: "user1",
135-
attributes: ["foo": "bar"],
136-
cmabUUID: "uuid"
150+
ruleId: "abc", userId: "user1",
151+
attributes: ["foo": "bar"], cmabUUID: "uuid"
137152
) { result in
138-
switch result {
139-
case .success(_):
140-
XCTFail("Expected failure, got success")
141-
case .failure(let error):
142-
XCTAssertEqual(error as? CmabClientError, .invalidResponse)
153+
if case let .failure(error) = result {
154+
XCTAssertTrue("\(error)".contains("HTTP error code"))
155+
} else {
156+
XCTFail("Expected failure on HTTP error")
143157
}
144158
expectation.fulfill()
145159
}
146-
147-
waitForExpectations(timeout: 2, handler: nil)
160+
waitForExpectations(timeout: 2)
148161
}
149162

150-
func testFetchDecisionRetriesOnFailure() {
151-
let expectedVariationId = "variation-retry"
152-
var callCount = 0
153-
154-
let responseJSON: [String: Any] = [
155-
"predictions": [
156-
["variation_id": expectedVariationId]
157-
]
163+
func testFetchDecision_InvalidJson() {
164+
mockSession.responses = [
165+
(Data("not a json".utf8), HTTPURLResponse(url: URL(string: "https://prediction.cmab.optimizely.com/predict/abc")!,
166+
statusCode: 200, httpVersion: nil, headerFields: nil), nil)
158167
]
159-
let responseData = try! JSONSerialization.data(withJSONObject: responseJSON, options: [])
160168

161-
mockSession.onRequest = { _ in
162-
callCount += 1
163-
if callCount == 1 {
164-
self.mockSession.nextData = Data()
165-
self.mockSession.nextResponse = HTTPURLResponse(url: URL(string: "https://prediction.cmab.optimizely.com/predict/abc")!,
166-
statusCode: 500, httpVersion: nil, headerFields: nil)
167-
self.mockSession.nextError = nil
169+
let expectation = self.expectation(description: "Completion called")
170+
client.fetchDecision(
171+
ruleId: "abc", userId: "user1",
172+
attributes: ["foo": "bar"], cmabUUID: "uuid"
173+
) { result in
174+
if case let .failure(error) = result {
175+
XCTAssertTrue(error is CmabClientError)
176+
XCTAssertEqual(self.mockSession.callCount, 1)
168177
} else {
169-
self.mockSession.nextData = responseData
170-
self.mockSession.nextResponse = HTTPURLResponse(url: URL(string: "https://prediction.cmab.optimizely.com/predict/abc")!,
171-
statusCode: 200, httpVersion: nil, headerFields: nil)
172-
self.mockSession.nextError = nil
178+
XCTFail("Expected failure on invalid JSON")
173179
}
180+
expectation.fulfill()
174181
}
182+
waitForExpectations(timeout: 2)
183+
}
184+
185+
func testFetchDecision_Invalid_Response_Structure() {
186+
let responseJSON: [String: Any] = [ "not_predictions": [] ]
187+
let responseData = try! JSONSerialization.data(withJSONObject: responseJSON, options: [])
188+
mockSession.responses = [
189+
(responseData, HTTPURLResponse(url: URL(string: "https://prediction.cmab.optimizely.com/predict/abc")!,
190+
statusCode: 200, httpVersion: nil, headerFields: nil), nil)
191+
]
175192

176193
let expectation = self.expectation(description: "Completion called")
177-
178194
client.fetchDecision(
179-
ruleId: "abc",
180-
userId: "user1",
181-
attributes: ["foo": "bar"],
182-
cmabUUID: "uuid"
195+
ruleId: "abc", userId: "user1",
196+
attributes: ["foo": "bar"], cmabUUID: "uuid"
183197
) { result in
184-
switch result {
185-
case .success(let variationId):
186-
XCTAssertEqual(variationId, expectedVariationId)
187-
XCTAssertTrue(callCount >= 2)
188-
case .failure(let error):
189-
XCTFail("Expected success, got failure: \(error)")
198+
if case let .failure(error) = result {
199+
XCTAssertEqual(error as? CmabClientError, .invalidResponse)
200+
XCTAssertEqual(self.mockSession.callCount, 1)
201+
} else {
202+
XCTFail("Expected failure on invalid response structure")
190203
}
191204
expectation.fulfill()
192205
}
193-
194-
waitForExpectations(timeout: 3, handler: nil)
206+
waitForExpectations(timeout: 2)
195207
}
196208
}
197209

210+
// MARK: - MockURLSession for ordered responses
211+
198212
extension DefaultCmabClientTests {
199213
class MockURLSessionDataTask: URLSessionDataTask {
200214
private let closure: () -> Void
201215
override var state: URLSessionTask.State { .completed }
202-
init(closure: @escaping () -> Void) {
203-
self.closure = closure
204-
}
205-
206-
override func resume() {
207-
closure()
208-
}
216+
init(closure: @escaping () -> Void) { self.closure = closure }
217+
override func resume() { closure() }
209218
}
210219

211220
class MockURLSession: URLSession {
212221
typealias CompletionHandler = (Data?, URLResponse?, Error?) -> Void
213-
214-
var nextData: Data?
215-
var nextResponse: URLResponse?
216-
var nextError: Error?
217-
var onRequest: ((URLRequest) -> Void)?
218-
222+
var responses: [(Data?, URLResponse?, Error?)] = []
223+
var callCount = 0
224+
219225
override func dataTask(
220226
with request: URLRequest,
221227
completionHandler: @escaping CompletionHandler
222228
) -> URLSessionDataTask {
223-
onRequest?(request)
224-
return MockURLSessionDataTask {
225-
completionHandler(self.nextData, self.nextResponse, self.nextError)
226-
}
229+
230+
let idx = callCount
231+
callCount += 1
232+
let tuple = idx < responses.count ? responses[idx] : (nil, nil, nil)
233+
return MockURLSessionDataTask { completionHandler(tuple.0, tuple.1, tuple.2) }
227234
}
228235
}
229-
230236
}

0 commit comments

Comments
 (0)