Skip to content

Commit dbb4aa9

Browse files
jaeoptthomaszurkan-optimizely
authored andcommitted
add more tests for Bucketer (#117)
* add more tests for DecisionService (wip) * fix error messages * add tests for DecisionServices * add more tests for Bucketer * clean up test cases for multiple groups * fix all per Tom's refactoring on decisionService * fix merge conflicts * fix tvOS test build error
1 parent f29107f commit dbb4aa9

File tree

8 files changed

+757
-131
lines changed

8 files changed

+757
-131
lines changed

OptimizelySDK/Implementation/DefaultBucketer.swift

Lines changed: 31 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -24,44 +24,10 @@ class DefaultBucketer : OPTBucketer {
2424

2525
private lazy var logger = HandlerRegistryService.shared.injectLogger()
2626

27-
// [Jae]: let be configured after initialized (with custom DecisionHandler set up on OPTManger initialization)
2827
init() {
2928
MAX_HASH_VALUE = MAX_HASH_SEED << 32
3029
}
3130

32-
func bucketToExperiment(config:ProjectConfig, group: Group, bucketingId: String) -> Experiment? {
33-
let hashId = makeHashIdFromBucketingId(bucketingId: bucketingId, entityId: group.id)
34-
let bucketValue = self.generateBucketValue(bucketingId: hashId)
35-
36-
if group.trafficAllocation.count == 0 {
37-
// log error if there are no traffic allocation values
38-
logger?.log(level: .error, message: "Group \(group.id) has no traffic allocation")
39-
return nil;
40-
}
41-
42-
for trafficAllocation in group.trafficAllocation {
43-
if bucketValue <= trafficAllocation.endOfRange {
44-
let experimentId = trafficAllocation.entityId;
45-
let experiment = config.getExperiment(id: experimentId)
46-
47-
// propagate errors and logs for unknown experiment
48-
if let _ = experiment
49-
{
50-
}
51-
else {
52-
// log problem with experiment id
53-
logger?.log(level: .error, message: "Experiment Id \(experimentId) for experiment not in datafile")
54-
}
55-
return experiment;
56-
}
57-
}
58-
59-
// log error if invalid bucketing id
60-
logger?.log(level: .error, message: "Bucketing value \(bucketValue) not in traffic allocation")
61-
62-
return nil
63-
}
64-
6531
func bucketExperiment(config:ProjectConfig, experiment: Experiment, bucketingId: String) -> Variation? {
6632
var ok = true
6733
// check for mutex
@@ -89,11 +55,41 @@ class DefaultBucketer : OPTBucketer {
8955
else {
9056
// log message if the user is mutually excluded
9157
logger?.log(level: .error, message: "User not bucketed into variation. Mutually excluded via group \(group?.id ?? "unknown")")
92-
58+
9359
return nil;
9460
}
9561
}
9662

63+
func bucketToExperiment(config:ProjectConfig, group: Group, bucketingId: String) -> Experiment? {
64+
let hashId = makeHashIdFromBucketingId(bucketingId: bucketingId, entityId: group.id)
65+
let bucketValue = self.generateBucketValue(bucketingId: hashId)
66+
67+
if group.trafficAllocation.count == 0 {
68+
// log error if there are no traffic allocation values
69+
logger?.log(level: .error, message: "Group \(group.id) has no traffic allocation")
70+
return nil;
71+
}
72+
73+
for trafficAllocation in group.trafficAllocation {
74+
if bucketValue <= trafficAllocation.endOfRange {
75+
let experimentId = trafficAllocation.entityId;
76+
let experiment = config.getExperiment(id: experimentId)
77+
78+
// propagate errors and logs for unknown experiment
79+
if experiment == nil {
80+
// log problem with experiment id
81+
logger?.log(level: .error, message: "Experiment Id \(experimentId) for experiment not in datafile")
82+
}
83+
return experiment;
84+
}
85+
}
86+
87+
// log error if invalid bucketing id
88+
logger?.log(level: .error, message: "Bucketing value \(bucketValue) not in traffic allocation")
89+
90+
return nil
91+
}
92+
9793
func bucketToVariation(experiment:Experiment, bucketingId:String) -> Variation? {
9894
let hashId = makeHashIdFromBucketingId(bucketingId: bucketingId, entityId: experiment.id)
9995
let bucketValue = generateBucketValue(bucketingId: hashId)

OptimizelySDK/OptimizelySwiftSDK.xcodeproj/project.pbxproj

Lines changed: 74 additions & 60 deletions
Large diffs are not rendered by default.
Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
//
2+
// BucketTests_Base.swift
3+
// OptimizelySwiftSDK
4+
//
5+
// Created by Jae Kim on 3/26/19.
6+
// Copyright © 2019 Optimizely. All rights reserved.
7+
//
8+
9+
import XCTest
10+
11+
class BucketTests_Base: XCTestCase {
12+
13+
// MARK: - murmur-hash compliant
14+
15+
func testHashIsCompliant() {
16+
let experimentId = "1886780721"
17+
let bucketer = DefaultBucketer()
18+
// These test inputs/outputs should be reproduced exactly in all clients to make sure that they behave
19+
// consistently.
20+
let tests = [
21+
["userId": "ppid1", "experimentId": experimentId, "expect": 5254],
22+
["userId": "ppid2", "experimentId": experimentId, "expect": 4299],
23+
// Same PPID as previous, diff experiment ID
24+
["userId": "ppid2", "experimentId": "1886780722", "expect": 2434],
25+
["userId": "ppid3", "experimentId": experimentId, "expect": 5439],
26+
["userId": "a very very very very very very very very very very very very very very very long ppd string", "experimentId": experimentId, "expect": 6128]];
27+
28+
for test in tests {
29+
let hashId = bucketer.makeHashIdFromBucketingId(bucketingId:test["userId"] as! String, entityId:test["experimentId"] as! String)
30+
let bucketingValue = bucketer.generateBucketValue(bucketingId: hashId)
31+
32+
XCTAssertEqual(test["expect"] as! Int, bucketingValue);
33+
}
34+
}
35+
36+
}
37+
Lines changed: 203 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,203 @@
1+
//
2+
// BucketTests_BucketVariation.swift
3+
// OptimizelySwiftSDK
4+
//
5+
// Created by Jae Kim on 3/26/19.
6+
// Copyright © 2019 Optimizely. All rights reserved.
7+
//
8+
9+
import XCTest
10+
11+
class BucketTests_BucketVariation: XCTestCase {
12+
13+
var optimizely: OptimizelyManager!
14+
var config: ProjectConfig!
15+
var bucketer: DefaultBucketer!
16+
17+
var kUserId = "12345"
18+
var kGroupId = "333333"
19+
var kExperimentId = "444444"
20+
21+
var kExperimentKey = "countryExperiment"
22+
23+
var kVariationKeyA = "a"
24+
var kVariationKeyB = "b"
25+
var kVariationKeyC = "c"
26+
var kVariationKeyD = "d"
27+
28+
var kVariationIdA = "a11"
29+
var kVariationIdB = "b11"
30+
var kVariationIdC = "c11"
31+
var kVariationIdD = "d11"
32+
33+
var kAudienceIdCountry = "10"
34+
var kAudienceIdAge = "20"
35+
var kAudienceIdInvalid = "9999999"
36+
37+
var kAttributesCountryMatch: [String: Any] = ["country": "us"]
38+
var kAttributesCountryNotMatch: [String: Any] = ["country": "ca"]
39+
var kAttributesAgeMatch: [String: Any] = ["age": 30]
40+
var kAttributesAgeNotMatch: [String: Any] = ["age": 10]
41+
var kAttributesEmpty: [String: Any] = [:]
42+
43+
var experiment: Experiment!
44+
var variation: Variation!
45+
46+
47+
// MARK: - Sample datafile data
48+
49+
var sampleExperimentData: [String: Any] { return
50+
[
51+
"status": "Running",
52+
"id": kExperimentId,
53+
"key": kExperimentKey,
54+
"layerId": "10420273888",
55+
"trafficAllocation": [
56+
["entityId": kVariationIdA, "endOfRange": 2500],
57+
["entityId": kVariationIdB, "endOfRange": 5000],
58+
["entityId": kVariationIdC, "endOfRange": 7500],
59+
["entityId": kVariationIdD, "endOfRange": 10000]
60+
],
61+
"audienceIds": [kAudienceIdCountry],
62+
"variations": [
63+
[
64+
"variables": [],
65+
"id": kVariationIdA,
66+
"key": kVariationKeyA
67+
],
68+
[
69+
"variables": [],
70+
"id": kVariationIdB,
71+
"key": kVariationKeyB
72+
],
73+
[
74+
"variables": [],
75+
"id": kVariationIdC,
76+
"key": kVariationKeyC
77+
],
78+
[
79+
"variables": [],
80+
"id": kVariationIdD,
81+
"key": kVariationKeyD
82+
]
83+
],
84+
"forcedVariations":[:],
85+
]
86+
}
87+
88+
var sampleGroupData: [String: Any] { return
89+
["id": kGroupId,
90+
"policy": "random",
91+
"trafficAllocation": [
92+
["entityId": kExperimentId, "endOfRange": 10000]
93+
],
94+
"experiments": [sampleExperimentData]
95+
]
96+
}
97+
98+
99+
// MARK: - Setup
100+
101+
override func setUp() {
102+
super.setUp()
103+
104+
self.optimizely = OTUtils.createOptimizely(datafileName: "empty_datafile",
105+
clearUserProfileService: true)
106+
self.config = self.optimizely.config!
107+
self.bucketer = ((optimizely.decisionService as! DefaultDecisionService).bucketer as! DefaultBucketer)
108+
}
109+
110+
}
111+
112+
// MARK: - bucket to variation (experiment)
113+
114+
extension BucketTests_BucketVariation {
115+
116+
func testBucketExperimentWithEmptyGroup() {
117+
experiment = try! OTUtils.model(from: sampleExperimentData)
118+
self.config.project.experiments = [experiment]
119+
120+
self.config.project.groups = []
121+
122+
let tests = [["userId": "ppid1", "expect": kVariationKeyB],
123+
["userId": "ppid2", "expect": kVariationKeyD],
124+
["userId": "ppid3", "expect": kVariationKeyA],
125+
["userId": "a very very very very very very very very very very very very very very very long ppd string", "expect": kVariationKeyC]]
126+
127+
for (idx, test) in tests.enumerated() {
128+
variation = bucketer.bucketExperiment(config: self.config, experiment: experiment, bucketingId: test["userId"]!)
129+
if let _ = test["expect"] {
130+
XCTAssertEqual(test["expect"], variation?.key, "test[\(idx)] failed")
131+
} else {
132+
XCTAssertNil(experiment);
133+
}
134+
}
135+
}
136+
137+
func testBucketExperimentWithGroupMatched() {
138+
experiment = try! OTUtils.model(from: sampleExperimentData)
139+
self.config.project.experiments = [experiment]
140+
141+
let group: Group = try! OTUtils.model(from: sampleGroupData)
142+
self.config.project.groups = [group]
143+
144+
145+
let tests = [["userId": "ppid1", "expect": kVariationKeyB],
146+
["userId": "ppid2", "expect": kVariationKeyD],
147+
["userId": "ppid3", "expect": kVariationKeyA],
148+
["userId": "a very very very very very very very very very very very very very very very long ppd string", "expect": kVariationKeyC]]
149+
150+
for (idx, test) in tests.enumerated() {
151+
variation = bucketer.bucketExperiment(config: self.config, experiment: experiment, bucketingId: test["userId"]!)
152+
if let _ = test["expect"] {
153+
XCTAssertEqual(test["expect"], variation?.key, "test[\(idx)] failed")
154+
} else {
155+
XCTAssertNil(experiment);
156+
}
157+
}
158+
}
159+
160+
func testBucketExperimentWithGroupNotMatched() {
161+
experiment = try! OTUtils.model(from: sampleExperimentData)
162+
self.config.project.experiments = [experiment]
163+
164+
var group: Group = try! OTUtils.model(from: sampleGroupData)
165+
group.trafficAllocation[0].endOfRange = 0
166+
self.config.project.groups = [group]
167+
168+
let tests = [["userId": "ppid1", "expect": kVariationKeyC],
169+
["userId": "ppid2", "expect": kVariationKeyC],
170+
["userId": "ppid3", "expect": kVariationKeyA],
171+
["userId": "a very very very very very very very very very very very very very very very long ppd string", "expect": kVariationKeyD]]
172+
173+
for test in tests {
174+
variation = bucketer.bucketExperiment(config: self.config, experiment: experiment, bucketingId: test["userId"]!)
175+
XCTAssertNil(variation)
176+
}
177+
}
178+
179+
func testBucketExperimentWithGroupNotRandom() {
180+
experiment = try! OTUtils.model(from: sampleExperimentData)
181+
self.config.project.experiments = [experiment]
182+
183+
var group: Group = try! OTUtils.model(from: sampleGroupData)
184+
group.policy = .overlapping
185+
self.config.project.groups = [group]
186+
187+
188+
let tests = [["userId": "ppid1", "expect": kVariationKeyB],
189+
["userId": "ppid2", "expect": kVariationKeyD],
190+
["userId": "ppid3", "expect": kVariationKeyA],
191+
["userId": "a very very very very very very very very very very very very very very very long ppd string", "expect": kVariationKeyC]]
192+
193+
for (idx, test) in tests.enumerated() {
194+
variation = bucketer.bucketExperiment(config: self.config, experiment: experiment, bucketingId: test["userId"]!)
195+
if let _ = test["expect"] {
196+
XCTAssertEqual(test["expect"], variation?.key, "test[\(idx)] failed")
197+
} else {
198+
XCTAssertNil(experiment);
199+
}
200+
}
201+
}
202+
203+
}

0 commit comments

Comments
 (0)