Skip to content

Commit fc15409

Browse files
wip: add unit test for bucket to variation with 10% traffic for holdout
1 parent 5b3e6f0 commit fc15409

File tree

3 files changed

+166
-0
lines changed

3 files changed

+166
-0
lines changed

OptimizelySwiftSDK.xcodeproj/project.pbxproj

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2052,6 +2052,8 @@
20522052
98AC97F12DAE4579001405DD /* HoldoutConfig.swift in Sources */ = {isa = PBXBuildFile; fileRef = 98AC97E12DAE4579001405DD /* HoldoutConfig.swift */; };
20532053
98AC97F32DAE9685001405DD /* HoldoutConfigTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 98AC97F22DAE9685001405DD /* HoldoutConfigTests.swift */; };
20542054
98AC97F42DAE9685001405DD /* HoldoutConfigTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 98AC97F22DAE9685001405DD /* HoldoutConfigTests.swift */; };
2055+
98AC98462DB7B762001405DD /* BucketTests_HoldoutToVariation.swift in Sources */ = {isa = PBXBuildFile; fileRef = 98AC98452DB7B762001405DD /* BucketTests_HoldoutToVariation.swift */; };
2056+
98AC98472DB7B762001405DD /* BucketTests_HoldoutToVariation.swift in Sources */ = {isa = PBXBuildFile; fileRef = 98AC98452DB7B762001405DD /* BucketTests_HoldoutToVariation.swift */; };
20552057
BD1C3E8524E4399C0084B4DA /* SemanticVersion.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0B97DD93249D327F003DE606 /* SemanticVersion.swift */; };
20562058
BD64853C2491474500F30986 /* Optimizely.h in Headers */ = {isa = PBXBuildFile; fileRef = 6E75167A22C520D400B2B157 /* Optimizely.h */; settings = {ATTRIBUTES = (Public, ); }; };
20572059
BD64853E2491474500F30986 /* Audience.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E75169822C520D400B2B157 /* Audience.swift */; };
@@ -2497,6 +2499,7 @@
24972499
987F11D92AF3F56F0083D3F9 /* PrivacyInfo.xcprivacy */ = {isa = PBXFileReference; lastKnownFileType = text.xml; path = PrivacyInfo.xcprivacy; sourceTree = "<group>"; };
24982500
98AC97E12DAE4579001405DD /* HoldoutConfig.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HoldoutConfig.swift; sourceTree = "<group>"; };
24992501
98AC97F22DAE9685001405DD /* HoldoutConfigTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HoldoutConfigTests.swift; sourceTree = "<group>"; };
2502+
98AC98452DB7B762001405DD /* BucketTests_HoldoutToVariation.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BucketTests_HoldoutToVariation.swift; sourceTree = "<group>"; };
25002503
BD6485812491474500F30986 /* Optimizely.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Optimizely.framework; sourceTree = BUILT_PRODUCTS_DIR; };
25012504
C78CAF572445AD8D009FE876 /* OptimizelyJSON.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OptimizelyJSON.swift; sourceTree = "<group>"; };
25022505
C78CAF652446DB91009FE876 /* OptimizelyClientTests_OptimizelyJSON.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OptimizelyClientTests_OptimizelyJSON.swift; sourceTree = "<group>"; };
@@ -3003,6 +3006,7 @@
30033006
6E75198F22C5211100B2B157 /* BucketTests_BucketVariation.swift */,
30043007
6E75198C22C5211100B2B157 /* BucketTests_ExpToVariation.swift */,
30053008
6E75198322C5211100B2B157 /* BucketTests_GroupToExp.swift */,
3009+
98AC98452DB7B762001405DD /* BucketTests_HoldoutToVariation.swift */,
30063010
6E75198422C5211100B2B157 /* BucketTests_Others.swift */,
30073011
6E75199622C5211100B2B157 /* DatafileHandlerTests.swift */,
30083012
6E75199822C5211100B2B157 /* DataStoreTests.swift */,
@@ -4901,6 +4905,7 @@
49014905
6E75192D22C520D500B2B157 /* DataStoreQueueStack.swift in Sources */,
49024906
6E7516D322C520D400B2B157 /* OPTLogger.swift in Sources */,
49034907
6E75180122C520D400B2B157 /* DataStoreUserDefaults.swift in Sources */,
4908+
98AC98472DB7B762001405DD /* BucketTests_HoldoutToVariation.swift in Sources */,
49044909
6E75175722C520D400B2B157 /* LogMessage.swift in Sources */,
49054910
6E7516EB22C520D400B2B157 /* OPTEventDispatcher.swift in Sources */,
49064911
6E75188522C520D400B2B157 /* TrafficAllocation.swift in Sources */,
@@ -5166,6 +5171,7 @@
51665171
6E75175D22C520D400B2B157 /* AtomicProperty.swift in Sources */,
51675172
6E7516D922C520D400B2B157 /* OPTUserProfileService.swift in Sources */,
51685173
6E7516E522C520D400B2B157 /* OPTEventDispatcher.swift in Sources */,
5174+
98AC98462DB7B762001405DD /* BucketTests_HoldoutToVariation.swift in Sources */,
51695175
6E7E9B552523F8C6009E4426 /* OptimizelyUserContextTests_Decide_Legacy.swift in Sources */,
51705176
6E652305278E688B00954EA1 /* LruCache.swift in Sources */,
51715177
6EC6DD3524ABF6990017D296 /* OptimizelyClient+Decide.swift in Sources */,

Sources/Implementation/DefaultBucketer.swift

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -150,6 +150,7 @@ class DefaultBucketer: OPTBucketer {
150150
}
151151

152152
func allocateTraffic(trafficAllocation: [TrafficAllocation], bucketValue: Int) -> String? {
153+
print("Bucketed value \(bucketValue)")
153154
for bucket in trafficAllocation where bucketValue < bucket.endOfRange {
154155
return bucket.entityId
155156
}
Lines changed: 159 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,159 @@
1+
//
2+
// Copyright 2019, 2021, Optimizely, Inc. and contributors
3+
//
4+
// Licensed under the Apache License, Version 2.0 (the "License");
5+
// you may not use this file except in compliance with the License.
6+
// You may obtain a copy of the License at
7+
//
8+
// http://www.apache.org/licenses/LICENSE-2.0
9+
//
10+
// Unless required by applicable law or agreed to in writing, software
11+
// distributed under the License is distributed on an "AS IS" BASIS,
12+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
// See the License for the specific language governing permissions and
14+
// limitations under the License.
15+
//
16+
17+
import XCTest
18+
19+
class BucketTests_HoldoutToVariation: XCTestCase {
20+
var optimizely: OptimizelyClient!
21+
var config: ProjectConfig!
22+
var bucketer: DefaultBucketer!
23+
24+
var kUserId = "123456"
25+
var kHoldoutId = "4444444"
26+
var kHoldoutKey = "holdout_key"
27+
28+
var kVariationKeyA = "a"
29+
var kVariationIdA = "a11"
30+
31+
var kAudienceIdCountry = "10"
32+
var kAudienceIdAge = "20"
33+
var kAudienceIdInvalid = "9999999"
34+
35+
var kAttributesCountryMatch: [String: Any] = ["country": "us"]
36+
var kAttributesCountryNotMatch: [String: Any] = ["country": "ca"]
37+
var kAttributesAgeMatch: [String: Any] = ["age": 30]
38+
var kAttributesAgeNotMatch: [String: Any] = ["age": 10]
39+
var kAttributesEmpty: [String: Any] = [:]
40+
41+
var holdout: Holdout!
42+
43+
// MARK: - Sample datafile data
44+
45+
var sampleHoldoutData: [String: Any] {
46+
return [
47+
"status": "Running",
48+
"id": kHoldoutId,
49+
"key": kHoldoutKey,
50+
"layerId": "10420273888",
51+
"trafficAllocation": [
52+
["entityId": kVariationIdA, "endOfRange": 1000] // 10% traffic allocation (0-1000 out of 10000)
53+
],
54+
"audienceIds": [kAudienceIdCountry],
55+
"variations": [
56+
["variables": [], "id": kVariationIdA, "key": kVariationKeyA]
57+
],
58+
]
59+
}
60+
61+
// MARK: - Setup
62+
63+
override func setUp() {
64+
super.setUp()
65+
66+
self.optimizely = OTUtils.createOptimizely(datafileName: "empty_datafile",
67+
clearUserProfileService: true)
68+
self.config = self.optimizely.config!
69+
self.bucketer = ((optimizely.decisionService as! DefaultDecisionService).bucketer as! DefaultBucketer)
70+
71+
// Initialize holdout
72+
holdout = try! OTUtils.model(from: sampleHoldoutData)
73+
}
74+
75+
// MARK: - Tests for bucketToVariation
76+
77+
func testBucketToVariation_ValidBucketingWithinAllocation() {
78+
// Test users that should bucket into the single variation (within 0-1000 range)
79+
let testCases = [
80+
["userId": "user1", "expectedVariation": kVariationKeyA], // Buckets to variation A
81+
["userId": "testuser", "expectedVariation": kVariationKeyA] // Buckets to variation A
82+
]
83+
84+
for (index, test) in testCases.enumerated() {
85+
// Mock bucket value to ensure it falls within 0-1000
86+
let mockBucketValue = 500 // Within 10% allocation
87+
let mockBucketer = Mockbucketer(mockBucketValue: mockBucketValue)
88+
let response = mockBucketer.bucketToVariation(experiment: holdout, bucketingId: test["userId"]!)
89+
XCTAssertNotNil(response.result, "Variation should not be nil for test case \(index)")
90+
XCTAssertEqual(response.result?.key, test["expectedVariation"], "Wrong variation for test case \(index)")
91+
}
92+
}
93+
94+
func testBucketToVariation_BucketValueOutsideAllocation() {
95+
// Test users that fall outside the 10% allocation (bucket value > 1000)
96+
let testCases = [
97+
["userId": "user2"],
98+
["userId": "anotheruser"]
99+
]
100+
101+
for (index, test) in testCases.enumerated() {
102+
// Mock bucket value to ensure it falls outside 0-1000
103+
let mockBucketValue = 1500 // Outside 10% allocation
104+
let mockBucketer = Mockbucketer(mockBucketValue: mockBucketValue)
105+
let response = mockBucketer.bucketToVariation(experiment: holdout, bucketingId: test["userId"]!)
106+
XCTAssertNil(response.result, "Variation should be nil for test case \(index) when outside allocation")
107+
}
108+
}
109+
110+
func testBucketToVariation_NoTrafficAllocation() {
111+
// Create a holdout with empty traffic allocation
112+
var modifiedHoldoutData = sampleHoldoutData
113+
modifiedHoldoutData["trafficAllocation"] = []
114+
let modifiedHoldout = try! OTUtils.model(from: modifiedHoldoutData) as Holdout
115+
116+
let response = bucketer.bucketToVariation(experiment: modifiedHoldout, bucketingId: kUserId)
117+
118+
XCTAssertNil(response.result, "Variation should be nil when no traffic allocation")
119+
}
120+
121+
func testBucketToVariation_InvalidVariationId() {
122+
// Create a holdout with invalid variation ID in traffic allocation
123+
var modifiedHoldoutData = sampleHoldoutData
124+
modifiedHoldoutData["trafficAllocation"] = [
125+
["entityId": "invalid_variation_id", "endOfRange": 1000]
126+
]
127+
let modifiedHoldout = try! OTUtils.model(from: modifiedHoldoutData) as Holdout
128+
129+
let response = bucketer.bucketToVariation(experiment: modifiedHoldout, bucketingId: kUserId)
130+
131+
XCTAssertNil(response.result, "Variation should be nil for invalid variation ID")
132+
}
133+
134+
func testBucketToVariation_EmptyBucketingId() {
135+
// Test with empty bucketing ID, still within allocation
136+
let mockBucketValue = 500
137+
let mockBucketer = Mockbucketer(mockBucketValue: mockBucketValue)
138+
let response = mockBucketer.bucketToVariation(experiment: holdout, bucketingId: "")
139+
140+
XCTAssertNotNil(response.result, "Should still bucket with empty bucketing ID")
141+
XCTAssertEqual(response.result?.key, kVariationKeyA, "Should bucket to variation A")
142+
}
143+
}
144+
145+
// MARK: - Helper for mocking bucket value
146+
147+
class Mockbucketer: DefaultBucketer {
148+
var mockBucketValue: Int
149+
150+
init(mockBucketValue: Int) {
151+
self.mockBucketValue = mockBucketValue
152+
super.init()
153+
}
154+
155+
override func generateBucketValue(bucketingId: String) -> Int {
156+
print(mockBucketValue)
157+
return mockBucketValue
158+
}
159+
}

0 commit comments

Comments
 (0)