Skip to content

Commit deb76cd

Browse files
[FSSDK-11374] add test cases for impression event and decision listener with holdout support (#588)
Add test cases for impression event and decision listener
1 parent 51b0387 commit deb76cd

File tree

6 files changed

+493
-4
lines changed

6 files changed

+493
-4
lines changed

OptimizelySwiftSDK.xcodeproj/project.pbxproj

+6
Original file line numberDiff line numberDiff line change
@@ -2046,6 +2046,8 @@
20462046
989428BC2DBFA431008BA1C8 /* MockBucketer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 989428B22DBFA431008BA1C8 /* MockBucketer.swift */; };
20472047
989428BD2DBFA431008BA1C8 /* MockBucketer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 989428B22DBFA431008BA1C8 /* MockBucketer.swift */; };
20482048
989428BE2DBFA431008BA1C8 /* MockBucketer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 989428B22DBFA431008BA1C8 /* MockBucketer.swift */; };
2049+
989428C02DBFDCE5008BA1C8 /* DecisionListenerTest_Holdouts.swift in Sources */ = {isa = PBXBuildFile; fileRef = 989428BF2DBFDCE5008BA1C8 /* DecisionListenerTest_Holdouts.swift */; };
2050+
989428C12DBFDCE5008BA1C8 /* DecisionListenerTest_Holdouts.swift in Sources */ = {isa = PBXBuildFile; fileRef = 989428BF2DBFDCE5008BA1C8 /* DecisionListenerTest_Holdouts.swift */; };
20492051
98AC97E22DAE4579001405DD /* HoldoutConfig.swift in Sources */ = {isa = PBXBuildFile; fileRef = 98AC97E12DAE4579001405DD /* HoldoutConfig.swift */; };
20502052
98AC97E32DAE4579001405DD /* HoldoutConfig.swift in Sources */ = {isa = PBXBuildFile; fileRef = 98AC97E12DAE4579001405DD /* HoldoutConfig.swift */; };
20512053
98AC97E42DAE4579001405DD /* HoldoutConfig.swift in Sources */ = {isa = PBXBuildFile; fileRef = 98AC97E12DAE4579001405DD /* HoldoutConfig.swift */; };
@@ -2516,6 +2518,7 @@
25162518
984FE5102CC8AA88004F6F41 /* UserProfileTracker.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserProfileTracker.swift; sourceTree = "<group>"; };
25172519
987F11D92AF3F56F0083D3F9 /* PrivacyInfo.xcprivacy */ = {isa = PBXFileReference; lastKnownFileType = text.xml; path = PrivacyInfo.xcprivacy; sourceTree = "<group>"; };
25182520
989428B22DBFA431008BA1C8 /* MockBucketer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MockBucketer.swift; sourceTree = "<group>"; };
2521+
989428BF2DBFDCE5008BA1C8 /* DecisionListenerTest_Holdouts.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DecisionListenerTest_Holdouts.swift; sourceTree = "<group>"; };
25192522
98AC97E12DAE4579001405DD /* HoldoutConfig.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HoldoutConfig.swift; sourceTree = "<group>"; };
25202523
98AC97F22DAE9685001405DD /* HoldoutConfigTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HoldoutConfigTests.swift; sourceTree = "<group>"; };
25212524
98AC98452DB7B762001405DD /* BucketTests_HoldoutToVariation.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BucketTests_HoldoutToVariation.swift; sourceTree = "<group>"; };
@@ -3033,6 +3036,7 @@
30333036
6E75199622C5211100B2B157 /* DatafileHandlerTests.swift */,
30343037
6E75199822C5211100B2B157 /* DataStoreTests.swift */,
30353038
6E75199022C5211100B2B157 /* DecisionListenerTests.swift */,
3039+
989428BF2DBFDCE5008BA1C8 /* DecisionListenerTest_Holdouts.swift */,
30363040
6E981FC1232C363300FADDD6 /* DecisionListenerTests_Datafile.swift */,
30373041
6E27ECBD266FD78600B4A6D4 /* DecisionReasonsTests.swift */,
30383042
6E75198022C5211100B2B157 /* DecisionServiceTests_Experiments.swift */,
@@ -4910,6 +4914,7 @@
49104914
6EC6DD4C24ABF89B0017D296 /* OptimizelyUserContext.swift in Sources */,
49114915
8464087D28130D3200CCF97D /* Integration.swift in Sources */,
49124916
6E7517E922C520D400B2B157 /* DefaultDecisionService.swift in Sources */,
4917+
989428C02DBFDCE5008BA1C8 /* DecisionListenerTest_Holdouts.swift in Sources */,
49134918
6E5D12282638DDF4000ABFC3 /* MockEventDispatcher.swift in Sources */,
49144919
6E9B116A22C5487100C22D81 /* BucketTests_Base.swift in Sources */,
49154920
6E9B115F22C5487100C22D81 /* MurmurTests.swift in Sources */,
@@ -5192,6 +5197,7 @@
51925197
6E7517CB22C520D400B2B157 /* DefaultBucketer.swift in Sources */,
51935198
845945C1287758A000D13E11 /* OdpConfig.swift in Sources */,
51945199
8464087528130D3200CCF97D /* Integration.swift in Sources */,
5200+
989428C12DBFDCE5008BA1C8 /* DecisionListenerTest_Holdouts.swift in Sources */,
51955201
6E5D12202638DDF4000ABFC3 /* MockEventDispatcher.swift in Sources */,
51965202
6E9B115022C5486E00C22D81 /* BucketTests_Base.swift in Sources */,
51975203
6E9B114522C5486E00C22D81 /* MurmurTests.swift in Sources */,

Sources/Optimizely/OptimizelyClient.swift

+5-1
Original file line numberDiff line numberDiff line change
@@ -801,7 +801,11 @@ extension OptimizelyClient {
801801

802802
func shouldSendDecisionEvent(source: String, decision: FeatureDecision?) -> Bool {
803803
guard let config = self.config else { return false }
804-
return (source == Constants.DecisionSource.featureTest.rawValue && decision?.variation != nil) || config.sendFlagDecisions
804+
let allowedSources: [String] = [
805+
Constants.DecisionSource.featureTest.rawValue,
806+
Constants.DecisionSource.holdout.rawValue
807+
]
808+
return (allowedSources.contains(source) && decision?.variation != nil) || config.sendFlagDecisions
805809
}
806810

807811
func sendImpressionEvent(experiment: ExperimentCore?,

Tests/OptimizelyTests-Common/BatchEventBuilderTests_Events.swift

+190-1
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,26 @@ class BatchEventBuilderTests_Events: XCTestCase {
2727
var project: Project!
2828
let datafile = OTUtils.loadJSONDatafile("api_datafile")!
2929

30+
var sampleHoldout: [String: Any] {
31+
return [
32+
"status": "Running",
33+
"id": "holdout_4444444",
34+
"key": "holdout_key",
35+
"layerId": "10420273888",
36+
"trafficAllocation": [
37+
["entityId": "holdout_variation_a11", "endOfRange": 10000] // 100% traffic allocation
38+
],
39+
"audienceIds": [],
40+
"variations": [
41+
[
42+
"variables": [],
43+
"id": "holdout_variation_a11",
44+
"key": "holdout_a"
45+
]
46+
]
47+
]
48+
}
49+
3050
override func setUp() {
3151
eventDispatcher = MockEventDispatcher()
3252
optimizely = OTUtils.createOptimizely(datafileName: "audience_targeting",
@@ -38,6 +58,10 @@ class BatchEventBuilderTests_Events: XCTestCase {
3858
override func tearDown() {
3959
Utils.sdkVersion = OPTIMIZELYSDKVERSION
4060
Utils.swiftSdkClientName = "swift-sdk"
61+
optimizely?.close()
62+
optimizely = nil
63+
optimizely?.eventDispatcher = nil
64+
super.tearDown()
4165
}
4266

4367
func testCreateImpressionEvent() {
@@ -461,6 +485,164 @@ extension BatchEventBuilderTests_Events {
461485
}
462486
}
463487

488+
// MARK:- Holdouts
489+
490+
extension BatchEventBuilderTests_Events {
491+
func testImpressionEvent_UserInHoldout() {
492+
let eventDispatcher2 = MockEventDispatcher()
493+
var optimizely: OptimizelyClient! = OptimizelyClient(sdkKey: "12345", eventDispatcher: eventDispatcher2)
494+
495+
try! optimizely.start(datafile: datafile)
496+
497+
let holdout: Holdout = try! OTUtils.model(from: sampleHoldout)
498+
optimizely.config?.project.holdouts = [holdout]
499+
500+
let exp = expectation(description: "Wait for event to dispatch")
501+
let user = optimizely.createUserContext(userId: userId)
502+
_ = user.decide(key: featureKey)
503+
504+
DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) {
505+
exp.fulfill()
506+
}
507+
508+
let result = XCTWaiter.wait(for: [exp], timeout: 0.2)
509+
if result == XCTWaiter.Result.completed {
510+
let event = getFirstEventJSON(client: optimizely)!
511+
let visitor = (event["visitors"] as! Array<Dictionary<String, Any>>)[0]
512+
let snapshot = (visitor["snapshots"] as! Array<Dictionary<String, Any>>)[0]
513+
let decision = (snapshot["decisions"] as! Array<Dictionary<String, Any>>)[0]
514+
515+
let metaData = decision["metadata"] as! Dictionary<String, Any>
516+
XCTAssertEqual(metaData["rule_type"] as! String, Constants.DecisionSource.holdout.rawValue)
517+
XCTAssertEqual(metaData["rule_key"] as! String, "holdout_key")
518+
XCTAssertEqual(metaData["flag_key"] as! String, "feature_1")
519+
XCTAssertEqual(metaData["variation_key"] as! String, "holdout_a")
520+
XCTAssertFalse(metaData["enabled"] as! Bool)
521+
} else {
522+
XCTFail("No event found")
523+
}
524+
525+
}
526+
527+
func testImpressionEvent_UserInHoldout_IncludedFlags() {
528+
let eventDispatcher2 = MockEventDispatcher()
529+
var optimizely: OptimizelyClient! = OptimizelyClient(sdkKey: "12345", eventDispatcher: eventDispatcher2)
530+
531+
try! optimizely.start(datafile: datafile)
532+
533+
var holdout: Holdout = try! OTUtils.model(from: sampleHoldout)
534+
holdout.includedFlags = ["4482920077"]
535+
optimizely.config?.project.holdouts = [holdout]
536+
537+
let exp = expectation(description: "Wait for event to dispatch")
538+
539+
let user = optimizely.createUserContext(userId: userId)
540+
_ = user.decide(key: featureKey)
541+
542+
543+
// Add a delay before evaluating getFirstEventJSON
544+
DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) {
545+
exp.fulfill() // Fulfill the expectation after the delay
546+
}
547+
548+
let result = XCTWaiter.wait(for: [exp], timeout: 0.2)
549+
if result == XCTWaiter.Result.completed {
550+
let event = getFirstEventJSON(client: optimizely)!
551+
let visitor = (event["visitors"] as! Array<Dictionary<String, Any>>)[0]
552+
let snapshot = (visitor["snapshots"] as! Array<Dictionary<String, Any>>)[0]
553+
let decision = (snapshot["decisions"] as! Array<Dictionary<String, Any>>)[0]
554+
555+
let metaData = decision["metadata"] as! Dictionary<String, Any>
556+
XCTAssertEqual(metaData["rule_type"] as! String, Constants.DecisionSource.holdout.rawValue)
557+
XCTAssertEqual(metaData["rule_key"] as! String, "holdout_key")
558+
XCTAssertEqual(metaData["flag_key"] as! String, "feature_1")
559+
XCTAssertEqual(metaData["variation_key"] as! String, "holdout_a")
560+
XCTAssertFalse(metaData["enabled"] as! Bool)
561+
} else {
562+
XCTFail("No event found")
563+
}
564+
optimizely = nil
565+
566+
}
567+
568+
func testImpressionEvent_UserNotInHoldout_ExcludedFlags() {
569+
let eventDispatcher2 = MockEventDispatcher()
570+
var optimizely: OptimizelyClient! = OptimizelyClient(sdkKey: "123456", eventDispatcher: eventDispatcher2)
571+
572+
try! optimizely.start(datafile: datafile)
573+
574+
var holdout: Holdout = try! OTUtils.model(from: sampleHoldout)
575+
holdout.excludedFlags = ["4482920077"]
576+
optimizely.config?.project.holdouts = [holdout]
577+
578+
let exp = expectation(description: "Wait for event to dispatch")
579+
580+
let user = optimizely.createUserContext(userId: userId)
581+
_ = user.decide(key: featureKey)
582+
583+
// Add a delay before evaluating getFirstEventJSON
584+
DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) {
585+
exp.fulfill() // Fulfill the expectation after the delay
586+
}
587+
588+
let result = XCTWaiter.wait(for: [exp], timeout: 0.2)
589+
if result == XCTWaiter.Result.completed {
590+
let event = getFirstEventJSON(client: optimizely)!
591+
let visitor = (event["visitors"] as! Array<Dictionary<String, Any>>)[0]
592+
let snapshot = (visitor["snapshots"] as! Array<Dictionary<String, Any>>)[0]
593+
let decision = (snapshot["decisions"] as! Array<Dictionary<String, Any>>)[0]
594+
595+
let metaData = decision["metadata"] as! Dictionary<String, Any>
596+
XCTAssertEqual(metaData["rule_type"] as! String, Constants.DecisionSource.featureTest.rawValue)
597+
XCTAssertEqual(metaData["rule_key"] as! String, "exp_with_audience")
598+
XCTAssertEqual(metaData["flag_key"] as! String, "feature_1")
599+
XCTAssertEqual(metaData["variation_key"] as! String, "a")
600+
XCTAssertTrue(metaData["enabled"] as! Bool)
601+
} else {
602+
XCTFail("No event found")
603+
}
604+
}
605+
606+
func testImpressionEvent_UserNotInHoldout_MissesTrafficAllocation() {
607+
let eventDispatcher2 = MockEventDispatcher()
608+
var optimizely: OptimizelyClient! = OptimizelyClient(sdkKey: "123457", eventDispatcher: eventDispatcher2)
609+
610+
try! optimizely.start(datafile: datafile)
611+
612+
var holdout: Holdout = try! OTUtils.model(from: sampleHoldout)
613+
/// Set traffic allocation to gero
614+
holdout.trafficAllocation[0].endOfRange = 0
615+
holdout.includedFlags = ["4482920077"]
616+
optimizely.config?.project.holdouts = [holdout]
617+
618+
let exp = expectation(description: "Wait for event to dispatch")
619+
620+
let user = optimizely.createUserContext(userId: userId)
621+
_ = user.decide(key: featureKey)
622+
623+
DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) {
624+
exp.fulfill() // Fulfill the expectation after the delay
625+
}
626+
627+
let result = XCTWaiter.wait(for: [exp], timeout: 0.2)
628+
if result == XCTWaiter.Result.completed {
629+
let event = getFirstEventJSON(client: optimizely)!
630+
let visitor = (event["visitors"] as! Array<Dictionary<String, Any>>)[0]
631+
let snapshot = (visitor["snapshots"] as! Array<Dictionary<String, Any>>)[0]
632+
let decision = (snapshot["decisions"] as! Array<Dictionary<String, Any>>)[0]
633+
634+
let metaData = decision["metadata"] as! Dictionary<String, Any>
635+
XCTAssertEqual(metaData["rule_type"] as! String, Constants.DecisionSource.featureTest.rawValue)
636+
XCTAssertEqual(metaData["rule_key"] as! String, "exp_with_audience")
637+
XCTAssertEqual(metaData["flag_key"] as! String, "feature_1")
638+
XCTAssertEqual(metaData["variation_key"] as! String, "a")
639+
XCTAssertTrue(metaData["enabled"] as! Bool)
640+
} else {
641+
XCTFail("No event found")
642+
}
643+
}
644+
}
645+
464646
// MARK: - Utils
465647

466648
extension BatchEventBuilderTests_Events {
@@ -477,7 +659,14 @@ extension BatchEventBuilderTests_Events {
477659
return json
478660
}
479661

480-
func getEventJSON(data: Data) -> [String: Any]? {
662+
func getFirstEventJSON(client: OptimizelyClient) -> [String: Any]? {
663+
guard let event = getFirstEvent(dispatcher: client.eventDispatcher as! MockEventDispatcher) else { return nil }
664+
665+
let json = try! JSONSerialization.jsonObject(with: event.body, options: .allowFragments) as! [String: Any]
666+
return json
667+
}
668+
669+
func getEventJSON(data: Data) -> [String: Any]? {
481670
let json = try! JSONSerialization.jsonObject(with: data, options: .allowFragments) as! [String: Any]
482671
return json
483672
}

0 commit comments

Comments
 (0)