25
25
using OptimizelySDK . Config ;
26
26
using OptimizelySDK . Entity ;
27
27
using OptimizelySDK . ErrorHandler ;
28
+ using OptimizelySDK . Event ;
29
+ using OptimizelySDK . Event . Entity ;
28
30
using OptimizelySDK . Logger ;
29
31
using OptimizelySDK . OptimizelyDecisions ;
30
32
@@ -34,6 +36,7 @@ namespace OptimizelySDK.Tests
34
36
public class DecisionServiceHoldoutTest
35
37
{
36
38
private Mock < ILogger > LoggerMock ;
39
+ private Mock < EventProcessor > EventProcessorMock ;
37
40
private DecisionService DecisionService ;
38
41
private DatafileProjectConfig Config ;
39
42
private JObject TestData ;
@@ -46,6 +49,7 @@ public class DecisionServiceHoldoutTest
46
49
public void Initialize ( )
47
50
{
48
51
LoggerMock = new Mock < ILogger > ( ) ;
52
+ EventProcessorMock = new Mock < EventProcessor > ( ) ;
49
53
50
54
// Load test data
51
55
var testDataPath = Path . Combine ( TestContext . CurrentContext . TestDirectory ,
@@ -242,5 +246,90 @@ public void TestGetVariationsForFeatureList_Holdout_DecisionReasons()
242
246
Assert . IsTrue ( decisionWithReasons . DecisionReasons . ToReport ( ) . Count > 0 , "Should have decision reasons" ) ;
243
247
}
244
248
}
249
+
250
+ [ Test ]
251
+ public void TestImpressionEventForHoldout ( )
252
+ {
253
+ var featureFlag = Config . FeatureKeyMap [ "test_flag_1" ] ;
254
+ var userAttributes = new UserAttributes ( ) ;
255
+
256
+ var eventDispatcher = new Event . Dispatcher . DefaultEventDispatcher ( LoggerMock . Object ) ;
257
+ var optimizelyWithMockedEvents = new Optimizely (
258
+ TestData [ "datafileWithHoldouts" ] . ToString ( ) ,
259
+ eventDispatcher ,
260
+ LoggerMock . Object ,
261
+ new ErrorHandler . NoOpErrorHandler ( ) ,
262
+ null , // userProfileService
263
+ false , // skipJsonValidation
264
+ EventProcessorMock . Object
265
+ ) ;
266
+
267
+ EventProcessorMock . Setup ( ep => ep . Process ( It . IsAny < ImpressionEvent > ( ) ) ) ;
268
+
269
+ var userContext = optimizelyWithMockedEvents . CreateUserContext ( TestUserId , userAttributes ) ;
270
+ var decision = userContext . Decide ( featureFlag . Key ) ;
271
+
272
+ Assert . IsNotNull ( decision , "Decision should not be null" ) ;
273
+ Assert . IsNotNull ( decision . RuleKey , "RuleKey should not be null" ) ;
274
+
275
+ var actualHoldout = Config . Holdouts ? . FirstOrDefault ( h => h . Key == decision . RuleKey ) ;
276
+
277
+ Assert . IsNotNull ( actualHoldout ,
278
+ $ "RuleKey '{ decision . RuleKey } ' should correspond to a holdout experiment") ;
279
+ Assert . AreEqual ( featureFlag . Key , decision . FlagKey , "Flag key should match" ) ;
280
+
281
+ var holdoutVariation = actualHoldout . Variations . FirstOrDefault ( v => v . Key == decision . VariationKey ) ;
282
+
283
+ Assert . IsNotNull ( holdoutVariation ,
284
+ $ "Variation '{ decision . VariationKey } ' should be from the chosen holdout '{ actualHoldout . Key } '") ;
285
+
286
+ Assert . AreEqual ( holdoutVariation . FeatureEnabled , decision . Enabled ,
287
+ "Enabled flag should match holdout variation's featureEnabled value" ) ;
288
+
289
+ EventProcessorMock . Verify ( ep => ep . Process ( It . IsAny < ImpressionEvent > ( ) ) , Times . Once ,
290
+ "Impression event should be processed exactly once for holdout decision" ) ;
291
+
292
+ EventProcessorMock . Verify ( ep => ep . Process ( It . Is < ImpressionEvent > ( ie =>
293
+ ie . Experiment . Key == actualHoldout . Key &&
294
+ ie . Experiment . Id == actualHoldout . Id &&
295
+ ie . Timestamp > 0 &&
296
+ ie . UserId == TestUserId
297
+ ) ) , Times . Once , "Impression event should contain correct holdout experiment details" ) ;
298
+ }
299
+
300
+ [ Test ]
301
+ public void TestImpressionEventForHoldout_DisableDecisionEvent ( )
302
+ {
303
+ var featureFlag = Config . FeatureKeyMap [ "test_flag_1" ] ;
304
+ var userAttributes = new UserAttributes ( ) ;
305
+
306
+ var eventDispatcher = new Event . Dispatcher . DefaultEventDispatcher ( LoggerMock . Object ) ;
307
+ var optimizelyWithMockedEvents = new Optimizely (
308
+ TestData [ "datafileWithHoldouts" ] . ToString ( ) ,
309
+ eventDispatcher ,
310
+ LoggerMock . Object ,
311
+ new ErrorHandler . NoOpErrorHandler ( ) ,
312
+ null , // userProfileService
313
+ false , // skipJsonValidation
314
+ EventProcessorMock . Object
315
+ ) ;
316
+
317
+ EventProcessorMock . Setup ( ep => ep . Process ( It . IsAny < ImpressionEvent > ( ) ) ) ;
318
+
319
+ var userContext = optimizelyWithMockedEvents . CreateUserContext ( TestUserId , userAttributes ) ;
320
+ var decision = userContext . Decide ( featureFlag . Key , new [ ] { OptimizelyDecideOption . DISABLE_DECISION_EVENT } ) ;
321
+
322
+ Assert . IsNotNull ( decision , "Decision should not be null" ) ;
323
+ Assert . IsNotNull ( decision . RuleKey , "User should be bucketed into a holdout" ) ;
324
+
325
+ var chosenHoldout = Config . Holdouts ? . FirstOrDefault ( h => h . Key == decision . RuleKey ) ;
326
+
327
+ Assert . IsNotNull ( chosenHoldout , $ "Holdout '{ decision . RuleKey } ' should exist in config") ;
328
+
329
+ Assert . AreEqual ( featureFlag . Key , decision . FlagKey , "Flag key should match" ) ;
330
+
331
+ EventProcessorMock . Verify ( ep => ep . Process ( It . IsAny < ImpressionEvent > ( ) ) , Times . Never ,
332
+ "No impression event should be processed when DISABLE_DECISION_EVENT option is used" ) ;
333
+ }
245
334
}
246
335
}
0 commit comments