Skip to content

Commit e445f7b

Browse files
wip: decide reasons test cases adde for holdout
1 parent e92a3b6 commit e445f7b

File tree

4 files changed

+236
-4
lines changed

4 files changed

+236
-4
lines changed

OptimizelySwiftSDK.xcodeproj/project.pbxproj

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2056,6 +2056,8 @@
20562056
98AC98472DB7B762001405DD /* BucketTests_HoldoutToVariation.swift in Sources */ = {isa = PBXBuildFile; fileRef = 98AC98452DB7B762001405DD /* BucketTests_HoldoutToVariation.swift */; };
20572057
98AC98492DB8FC29001405DD /* DecisionServiceTests_Features_Holdouts.swift in Sources */ = {isa = PBXBuildFile; fileRef = 98AC98482DB8FC29001405DD /* DecisionServiceTests_Features_Holdouts.swift */; };
20582058
98AC984B2DB8FFE0001405DD /* DecisionServiceTests_Features_Holdouts.swift in Sources */ = {isa = PBXBuildFile; fileRef = 98AC98482DB8FC29001405DD /* DecisionServiceTests_Features_Holdouts.swift */; };
2059+
98AC985E2DBA6721001405DD /* OptimizelyUserContextTests_Decide_With_Holdouts_Reasons.swift in Sources */ = {isa = PBXBuildFile; fileRef = 98AC985D2DBA6721001405DD /* OptimizelyUserContextTests_Decide_With_Holdouts_Reasons.swift */; };
2060+
98AC985F2DBA6721001405DD /* OptimizelyUserContextTests_Decide_With_Holdouts_Reasons.swift in Sources */ = {isa = PBXBuildFile; fileRef = 98AC985D2DBA6721001405DD /* OptimizelyUserContextTests_Decide_With_Holdouts_Reasons.swift */; };
20592061
BD1C3E8524E4399C0084B4DA /* SemanticVersion.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0B97DD93249D327F003DE606 /* SemanticVersion.swift */; };
20602062
BD64853C2491474500F30986 /* Optimizely.h in Headers */ = {isa = PBXBuildFile; fileRef = 6E75167A22C520D400B2B157 /* Optimizely.h */; settings = {ATTRIBUTES = (Public, ); }; };
20612063
BD64853E2491474500F30986 /* Audience.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E75169822C520D400B2B157 /* Audience.swift */; };
@@ -2503,6 +2505,7 @@
25032505
98AC97F22DAE9685001405DD /* HoldoutConfigTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HoldoutConfigTests.swift; sourceTree = "<group>"; };
25042506
98AC98452DB7B762001405DD /* BucketTests_HoldoutToVariation.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BucketTests_HoldoutToVariation.swift; sourceTree = "<group>"; };
25052507
98AC98482DB8FC29001405DD /* DecisionServiceTests_Features_Holdouts.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DecisionServiceTests_Features_Holdouts.swift; sourceTree = "<group>"; };
2508+
98AC985D2DBA6721001405DD /* OptimizelyUserContextTests_Decide_With_Holdouts_Reasons.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OptimizelyUserContextTests_Decide_With_Holdouts_Reasons.swift; sourceTree = "<group>"; };
25062509
BD6485812491474500F30986 /* Optimizely.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Optimizely.framework; sourceTree = BUILT_PRODUCTS_DIR; };
25072510
C78CAF572445AD8D009FE876 /* OptimizelyJSON.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OptimizelyJSON.swift; sourceTree = "<group>"; };
25082511
C78CAF652446DB91009FE876 /* OptimizelyClientTests_OptimizelyJSON.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OptimizelyClientTests_OptimizelyJSON.swift; sourceTree = "<group>"; };
@@ -3040,6 +3043,7 @@
30403043
6EC6DD6824AE94820017D296 /* OptimizelyUserContextTests.swift */,
30413044
6E2D34B8250AD14000A0CDFE /* OptimizelyUserContextTests_Decide.swift */,
30423045
6E7E9B362523F8BF009E4426 /* OptimizelyUserContextTests_Decide_Reasons.swift */,
3046+
98AC985D2DBA6721001405DD /* OptimizelyUserContextTests_Decide_With_Holdouts_Reasons.swift */,
30433047
6EB97BCC24C89DFB00068883 /* OptimizelyUserContextTests_Decide_Legacy.swift */,
30443048
6E0A72D326C5B9AE00FF92B5 /* OptimizelyUserContextTests_ForcedDecisions.swift */,
30453049
98137C562A42BA0F004896EB /* OptimizelyUserContextTests_ODP_Aync_Await.swift */,
@@ -4963,6 +4967,7 @@
49634967
6E27EC9C266EF11000B4A6D4 /* OptimizelyDecisionTests.swift in Sources */,
49644968
6E7518D922C520D400B2B157 /* AttributeValue.swift in Sources */,
49654969
6E9B116822C5487100C22D81 /* DefaultLoggerTests.swift in Sources */,
4970+
98AC985F2DBA6721001405DD /* OptimizelyUserContextTests_Decide_With_Holdouts_Reasons.swift in Sources */,
49664971
C78CAF622445AD8D009FE876 /* OptimizelyJSON.swift in Sources */,
49674972
6E75179322C520D400B2B157 /* OptimizelyClient+Extension.swift in Sources */,
49684973
6E9B117122C5487100C22D81 /* DecisionServiceTests_Features.swift in Sources */,
@@ -5241,6 +5246,7 @@
52415246
6E7518EB22C520D400B2B157 /* ConditionHolder.swift in Sources */,
52425247
6E27EC9B266EF11000B4A6D4 /* OptimizelyDecisionTests.swift in Sources */,
52435248
6E75176922C520D400B2B157 /* Utils.swift in Sources */,
5249+
98AC985E2DBA6721001405DD /* OptimizelyUserContextTests_Decide_With_Holdouts_Reasons.swift in Sources */,
52445250
6E9B114E22C5486E00C22D81 /* DefaultLoggerTests.swift in Sources */,
52455251
C78CAF5B2445AD8D009FE876 /* OptimizelyJSON.swift in Sources */,
52465252
6E7518C722C520D400B2B157 /* Audience.swift in Sources */,

Sources/Implementation/DefaultDecisionService.swift

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -387,12 +387,15 @@ class DefaultDecisionService: OPTDecisionService {
387387
holdout: Holdout,
388388
user: OptimizelyUserContext,
389389
options: [OptimizelyDecideOption]? = nil) -> DecisionResponse<Variation> {
390+
let reasons = DecisionReasons(options: options)
391+
390392
guard holdout.isActivated else {
391-
return DecisionResponse(result: nil, reasons: DecisionReasons(options: options))
393+
let info = LogMessage.holdoutNotRunning(holdout.key)
394+
reasons.addInfo(info)
395+
logger.i(info)
396+
return DecisionResponse(result: nil, reasons: reasons)
392397
}
393398

394-
let reasons = DecisionReasons(options: options)
395-
396399
// ---- check if the user passes audience targeting before bucketing ----
397400
let audienceResponse = doesMeetAudienceConditions(config: config,
398401
experiment: holdout,

Tests/OptimizelyTests-Common/OptimizelyUserContextTests_Decide_Reasons.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@
1717
import XCTest
1818

1919
class OptimizelyUserContextTests_Decide_Reasons: XCTestCase {
20-
20+
/// Need to add testcases for holdout
2121
let kUserId = "tester"
2222

2323
var optimizely: OptimizelyClient!
Lines changed: 223 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,223 @@
1+
//
2+
// Copyright 2022, 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 OptimizelyUserContextTests_Decide_With_Holdouts_Reasons: XCTestCase {
20+
let kUserId = "tester"
21+
var optimizely: OptimizelyClient!
22+
23+
var kAttributesCountryMatch: [String: Any] = ["country": "US"]
24+
var kAttributesCountryNotMatch: [String: Any] = ["country": "ca"]
25+
26+
var sampleHoldout: [String: Any] {
27+
return [
28+
"status": "Running",
29+
"id": "id_holdout",
30+
"key": "key_holdout",
31+
"layerId": "10420273888",
32+
"trafficAllocation": [
33+
["entityId": "id_holdout_variation", "endOfRange": 500]
34+
],
35+
"audienceIds": [],
36+
"variations": [
37+
[
38+
"variables": [],
39+
"id": "id_holdout_variation",
40+
"key": "key_holdout_variation"
41+
]
42+
],
43+
"includedFlags": [],
44+
"excludedFlags": []
45+
]
46+
}
47+
48+
override func setUp() {
49+
super.setUp()
50+
51+
optimizely = OptimizelyClient(sdkKey: OTUtils.randomSdkKey,
52+
userProfileService: OTUtils.createClearUserProfileService())
53+
54+
try! optimizely.start(datafile: OTUtils.loadJSONDatafile("decide_datafile")!)
55+
}
56+
57+
/// Test when user is bucketed into the global holdout
58+
func testDecideReasons_userBucketedIntoGlobalHoldout() {
59+
let featureKey = "feature_1"
60+
61+
let holdout = try! OTUtils.model(from: sampleHoldout) as Holdout
62+
optimizely.config!.project.holdouts = [holdout]
63+
64+
let mockDecisionService = DefaultDecisionService(userProfileService: OTUtils.createClearUserProfileService(), bucketer: MockBucketer(mockBucketValue: 400))
65+
optimizely.decisionService = mockDecisionService
66+
67+
let user = optimizely.createUserContext(userId: kUserId)
68+
// Call decide with reasons
69+
let decision = user.decide(key: featureKey, options: [.includeReasons])
70+
// Assertions
71+
XCTAssertEqual(decision.flagKey, "feature_1", "Expected flagKey to be 'feature_1'")
72+
XCTAssertEqual(decision.variationKey, "key_holdout_variation", "Expected variationKey to be 'key_holdout_variation'")
73+
XCTAssertFalse(decision.enabled, "Feature should be disabled in holdout")
74+
XCTAssert(decision.reasons.contains(LogMessage.userBucketedIntoVariationInHoldout(kUserId, "key_holdout", "key_holdout_variation").reason))
75+
}
76+
77+
/// Test when user is bucketed into the included flags holdout for feature_1
78+
func testDecideReasons_userBucketedIntoIncludedHoldout() {
79+
let featureKey = "feature_1"
80+
let featureId = "4482920077"
81+
82+
var holdout = try! OTUtils.model(from: sampleHoldout) as Holdout
83+
holdout.includedFlags = [featureId]
84+
optimizely.config!.project.holdouts = [holdout]
85+
86+
let mockDecisionService = DefaultDecisionService(userProfileService: OTUtils.createClearUserProfileService(), bucketer: MockBucketer(mockBucketValue: 400))
87+
optimizely.decisionService = mockDecisionService
88+
89+
let user = optimizely.createUserContext(userId: kUserId)
90+
// Call decide with reasons
91+
let decision = user.decide(key: featureKey, options: [.includeReasons])
92+
// Assertions
93+
XCTAssertEqual(decision.flagKey, "feature_1", "Expected flagKey to be 'feature_1'")
94+
XCTAssertEqual(decision.variationKey, "key_holdout_variation", "Expected variationKey to be 'key_holdout_variation'")
95+
XCTAssertFalse(decision.enabled, "Feature should be disabled in holdout")
96+
XCTAssert(decision.reasons.contains(LogMessage.userBucketedIntoVariationInHoldout(kUserId, "key_holdout", "key_holdout_variation").reason))
97+
}
98+
99+
/// Test when user is not bucketed into any holdout for feature_2 (excluded)
100+
func testDecideReasons_userNotBucketedIntoExcludedHoldout() {
101+
// Global holdout with 5% traffice
102+
let holdout1 = try! OTUtils.model(from: sampleHoldout) as Holdout
103+
104+
let featureKey_2 = "feature_2"
105+
let featureId_2 = "4482920078"
106+
107+
var holdout2 = holdout1
108+
holdout2.id = "id_holdout_2"
109+
holdout2.key = "key_holdout_2"
110+
111+
// Global holdout with 10% traffice (featureId_2 excluded)
112+
holdout2.trafficAllocation[0].endOfRange = 1000
113+
holdout2.excludedFlags = [featureId_2]
114+
115+
// Bucket valud outside global holdout range but inside second holdout range
116+
let mockDecisionService = DefaultDecisionService(userProfileService: OTUtils.createClearUserProfileService(), bucketer: MockBucketer(mockBucketValue: 600))
117+
optimizely.decisionService = mockDecisionService
118+
optimizely.config!.project.holdouts = [holdout1, holdout2]
119+
120+
let user = optimizely.createUserContext(userId: kUserId)
121+
// Call decide with reasons
122+
let decision = user.decide(key: featureKey_2, options: [.includeReasons])
123+
124+
// Assertions
125+
XCTAssertEqual(decision.flagKey, "feature_2", "Expected flagKey to be 'feature_2'")
126+
XCTAssert(decision.reasons.contains(LogMessage.userNotBucketedIntoHoldoutVariation(kUserId).reason))
127+
}
128+
129+
/// Test when holdout is not running
130+
func testDecideReasons_holdoutNotRunning() {
131+
let featureKey = "feature_1"
132+
133+
var holdout = try! OTUtils.model(from: sampleHoldout) as Holdout
134+
holdout.status = .draft
135+
optimizely.config!.project.holdouts = [holdout]
136+
137+
let mockDecisionService = DefaultDecisionService(userProfileService: OTUtils.createClearUserProfileService(), bucketer: MockBucketer(mockBucketValue: 400))
138+
optimizely.decisionService = mockDecisionService
139+
140+
let user = optimizely.createUserContext(userId: kUserId)
141+
142+
// Call decide with reasons
143+
let decision = user.decide(key: featureKey, options: [.includeReasons])
144+
145+
/// Doesn't get holdout decision, because holdout isn't running
146+
/// Get decision for feature flag 1
147+
XCTAssertEqual(decision.flagKey, "feature_1", "Expected flagKey to be 'feature_1'")
148+
XCTAssertEqual(decision.ruleKey, "18322080788")
149+
XCTAssertEqual(decision.variationKey, "18257766532")
150+
XCTAssertTrue(decision.enabled)
151+
XCTAssert(decision.reasons.contains(LogMessage.holdoutNotRunning("key_holdout").reason))
152+
}
153+
154+
155+
/// Test when user meets audience conditions for holdout
156+
func testDecideReasons_userDoesMeetConditionsForHoldout() {
157+
let featureKey = "feature_1"
158+
var holdout = try! OTUtils.model(from: sampleHoldout) as Holdout
159+
// Audience "13389130056" requires "country" = "US"
160+
holdout.audienceIds = ["13389130056"]
161+
162+
let mockDecisionService = DefaultDecisionService(userProfileService: OTUtils.createClearUserProfileService(), bucketer: MockBucketer(mockBucketValue: 400))
163+
optimizely.decisionService = mockDecisionService
164+
optimizely.config!.project.holdouts = [holdout]
165+
166+
167+
let user = optimizely.createUserContext(userId: kUserId, attributes: kAttributesCountryMatch)
168+
// Call decide with reasons
169+
let decision = user.decide(key: featureKey, options: [.includeReasons])
170+
171+
// Assertions
172+
XCTAssertEqual(decision.flagKey, "feature_1", "Expected flagKey to be 'feature_1'")
173+
XCTAssertEqual(decision.variationKey, "key_holdout_variation", "Expected variationKey to be 'key_holdout_variation'")
174+
XCTAssertFalse(decision.enabled, "Feature should be disabled in holdout")
175+
XCTAssert(decision.reasons.contains(LogMessage.userBucketedIntoVariationInHoldout(kUserId, "key_holdout", "key_holdout_variation").reason))
176+
XCTAssert(decision.reasons.contains(LogMessage.userMeetsConditionsForHoldout(kUserId, "key_holdout").reason))
177+
}
178+
179+
/// Test when user does not meet audience conditions for holdout
180+
func testDecideReasons_userDoesntMeetConditionsForHoldout() {
181+
let featureKey = "feature_1"
182+
var holdout = try! OTUtils.model(from: sampleHoldout) as Holdout
183+
// Audience "13389130056" requires "country" = "US"
184+
holdout.audienceIds = ["13389130056"]
185+
186+
let mockDecisionService = DefaultDecisionService(userProfileService: OTUtils.createClearUserProfileService(), bucketer: MockBucketer(mockBucketValue: 400))
187+
optimizely.decisionService = mockDecisionService
188+
optimizely.config!.project.holdouts = [holdout]
189+
190+
let user = optimizely.createUserContext(userId: kUserId, attributes: kAttributesCountryNotMatch)
191+
// Call decide with reasons
192+
let decision = user.decide(key: featureKey, options: [.includeReasons])
193+
194+
XCTAssertEqual(decision.flagKey, "feature_1", "Expected flagKey to be 'feature_1'")
195+
XCTAssertNotEqual(decision.variationKey, "key_holdout_variation", "Expected variationKey not to be 'key_holdout_variation'")
196+
XCTAssertFalse(decision.reasons.contains(LogMessage.userBucketedIntoVariationInHoldout(kUserId, "key_holdout", "key_holdout_variation").reason))
197+
XCTAssert(decision.reasons.contains(LogMessage.userDoesntMeetConditionsForHoldout(kUserId, "key_holdout").reason))
198+
}
199+
}
200+
201+
202+
// MARK: - Helper for mocking bucketer
203+
204+
class MockBucketer: DefaultBucketer {
205+
var mockBucketValue: Int
206+
207+
init(mockBucketValue: Int) {
208+
self.mockBucketValue = mockBucketValue
209+
super.init()
210+
}
211+
212+
override func generateBucketValue(bucketingId: String) -> Int {
213+
return mockBucketValue
214+
}
215+
}
216+
217+
// MARK: - Mock Decision Service
218+
219+
class MockDecisionService: DefaultDecisionService {
220+
init(bucketer: OPTBucketer, userProfileService: OPTUserProfileService) {
221+
super.init(userProfileService: userProfileService, bucketer: bucketer)
222+
}
223+
}

0 commit comments

Comments
 (0)