From 819a29fca9a2eb735837131bf337ad875a33c99e Mon Sep 17 00:00:00 2001 From: wangjoshuah Date: Wed, 5 Jul 2017 21:02:28 -0700 Subject: [PATCH 1/2] update OptimizelyTest.java tests to incorporate v4 parameters --- .../com/optimizely/ab/OptimizelyTest.java | 266 ++++++++++++++---- 1 file changed, 210 insertions(+), 56 deletions(-) diff --git a/core-api/src/test/java/com/optimizely/ab/OptimizelyTest.java b/core-api/src/test/java/com/optimizely/ab/OptimizelyTest.java index d557cc7dd..aa26faad2 100644 --- a/core-api/src/test/java/com/optimizely/ab/OptimizelyTest.java +++ b/core-api/src/test/java/com/optimizely/ab/OptimizelyTest.java @@ -19,6 +19,7 @@ import ch.qos.logback.classic.Level; import com.google.common.collect.ImmutableMap; import com.optimizely.ab.bucketing.Bucketer; +import com.optimizely.ab.bucketing.DecisionService; import com.optimizely.ab.config.Attribute; import com.optimizely.ab.config.EventType; import com.optimizely.ab.config.Experiment; @@ -49,6 +50,8 @@ import org.mockito.junit.MockitoRule; import java.io.IOException; +import java.nio.file.ProviderNotFoundException; +import java.util.ArrayList; import java.util.Arrays; import java.util.Collection; import java.util.Collections; @@ -62,8 +65,19 @@ import static com.optimizely.ab.config.ProjectConfigTestUtils.noAudienceProjectConfigV3; import static com.optimizely.ab.config.ProjectConfigTestUtils.validConfigJsonV2; import static com.optimizely.ab.config.ProjectConfigTestUtils.validConfigJsonV3; +import static com.optimizely.ab.config.ProjectConfigTestUtils.validConfigJsonV4; import static com.optimizely.ab.config.ProjectConfigTestUtils.validProjectConfigV2; import static com.optimizely.ab.config.ProjectConfigTestUtils.validProjectConfigV3; +import static com.optimizely.ab.config.ProjectConfigTestUtils.validProjectConfigV4; +import static com.optimizely.ab.config.ValidProjectConfigV4.ATTRIBUTE_HOUSE_KEY; +import static com.optimizely.ab.config.ValidProjectConfigV4.AUDIENCE_GRYFFINDOR_VALUE; +import static com.optimizely.ab.config.ValidProjectConfigV4.EVENT_BASIC_EVENT_KEY; +import static com.optimizely.ab.config.ValidProjectConfigV4.EXPERIMENT_LAUNCHED_EXPERIMENT_KEY; +import static com.optimizely.ab.config.ValidProjectConfigV4.EXPERIMENT_MULTIVARIATE_EXPERIMENT_KEY; +import static com.optimizely.ab.config.ValidProjectConfigV4.EXPERIMENT_PAUSED_EXPERIMENT_KEY; +import static com.optimizely.ab.config.ValidProjectConfigV4.MULTIVARIATE_EXPERIMENT_FORCED_VARIATION_USER_ID_GRED; +import static com.optimizely.ab.config.ValidProjectConfigV4.PAUSED_EXPERIMENT_FORCED_VARIATION_USER_ID_CONTROL; +import static com.optimizely.ab.config.ValidProjectConfigV4.VARIATION_MULTIVARIATE_EXPERIMENT_GRED_KEY; import static com.optimizely.ab.event.LogEvent.RequestMethod; import static com.optimizely.ab.event.internal.EventBuilderV2Test.createExperimentVariationMap; import static java.util.Arrays.asList; @@ -72,6 +86,7 @@ import static org.hamcrest.MatcherAssert.assertThat; import static org.hamcrest.Matchers.hasEntry; import static org.hamcrest.Matchers.hasKey; +import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertNotNull; import static org.junit.Assert.assertNull; import static org.mockito.Matchers.any; @@ -81,6 +96,7 @@ import static org.mockito.Mockito.doThrow; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.never; +import static org.mockito.Mockito.spy; import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; @@ -107,6 +123,13 @@ public static Collection data() throws IOException { noAudienceProjectConfigJsonV3(), validProjectConfigV3(), noAudienceProjectConfigV3() + }, + { + 4, + validConfigJsonV4(), + validConfigJsonV4(), + validProjectConfigV4(), + validProjectConfigV4() } }); } @@ -123,6 +146,7 @@ public static Collection data() throws IOException { @Mock EventHandler mockEventHandler; @Mock Bucketer mockBucketer; + @Mock DecisionService mockDecisionService; @Mock ErrorHandler mockErrorHandler; private static final String genericUserId = "genericUserId"; @@ -152,7 +176,16 @@ public OptimizelyTest(int datafileVersion, */ @Test public void activateEndToEnd() throws Exception { - Experiment activatedExperiment = validProjectConfig.getExperiments().get(0); + Experiment activatedExperiment; + Map testUserAttributes = new HashMap(); + if(datafileVersion == 4) { + activatedExperiment = validProjectConfig.getExperimentKeyMapping().get(EXPERIMENT_MULTIVARIATE_EXPERIMENT_KEY); + testUserAttributes.put(ATTRIBUTE_HOUSE_KEY, AUDIENCE_GRYFFINDOR_VALUE); + } + else { + activatedExperiment = validProjectConfig.getExperiments().get(0); + testUserAttributes.put("browser_type", "chrome"); + } Variation bucketedVariation = activatedExperiment.getVariations().get(0); EventBuilder mockEventBuilder = mock(EventBuilder.class); @@ -163,9 +196,6 @@ public void activateEndToEnd() throws Exception { .withErrorHandler(mockErrorHandler) .build(); - Map testUserAttributes = new HashMap(); - testUserAttributes.put("browser_type", "chrome"); - Map testParams = new HashMap(); testParams.put("test", "params"); LogEvent logEventToDispatch = new LogEvent(RequestMethod.GET, "test_url", testParams, ""); @@ -176,7 +206,8 @@ public void activateEndToEnd() throws Exception { when(mockBucketer.bucket(activatedExperiment, "userId")) .thenReturn(bucketedVariation); - logbackVerifier.expectMessage(Level.INFO, "Activating user \"userId\" in experiment \"etag1\"."); + logbackVerifier.expectMessage(Level.INFO, "Activating user \"userId\" in experiment \"" + + activatedExperiment.getKey() + "\"."); logbackVerifier.expectMessage(Level.DEBUG, "Dispatching impression event to URL test_url with params " + testParams + " and payload \"\""); @@ -211,7 +242,8 @@ public void activateForNullVariation() throws Exception { when(mockBucketer.bucket(activatedExperiment, "userId")) .thenReturn(null); - logbackVerifier.expectMessage(Level.INFO, "Not activating user \"userId\" for experiment \"etag1\"."); + logbackVerifier.expectMessage(Level.INFO, "Not activating user \"userId\" for experiment \"" + + activatedExperiment.getKey() + "\"."); // activate the experiment Variation actualVariation = optimizely.activate(activatedExperiment.getKey(), "userId", testUserAttributes); @@ -250,8 +282,9 @@ public void activateWhenExperimentIsNotInProject() throws Exception { } /** - * Verify that the {@link Optimizely#activate(String, String)} call correctly builds an endpoint url and - * request params and passes them through {@link EventHandler#dispatchEvent(LogEvent)}. + * Verify that the {@link Optimizely#activate(String, String, Map)} call + * correctly builds an endpoint url and request params + * and passes them through {@link EventHandler#dispatchEvent(LogEvent)}. */ @Test public void activateWithExperimentKey() throws Exception { @@ -267,7 +300,12 @@ public void activateWithExperimentKey() throws Exception { .build(); Map testUserAttributes = new HashMap(); - testUserAttributes.put("browser_type", "chrome"); + if (datafileVersion == 4) { + testUserAttributes.put(ATTRIBUTE_HOUSE_KEY, AUDIENCE_GRYFFINDOR_VALUE); + } + else { + testUserAttributes.put("browser_type", "chrome"); + } Map testParams = new HashMap(); testParams.put("test", "params"); @@ -383,8 +421,8 @@ public void activateWithAttributes() throws Exception { } /** - * Verify that {@link Optimizely#activate(String, String)} handles the case where an unknown attribute - * (i.e., not in the config) is passed through. + * Verify that {@link Optimizely#activate(String, String, Map)} handles the case + * where an unknown attribute (i.e., not in the config) is passed through. * * In this case, the activate call should remove the unknown attribute from the given map. */ @@ -405,7 +443,12 @@ public void activateWithUnknownAttribute() throws Exception { .build(); Map testUserAttributes = new HashMap(); - testUserAttributes.put("browser_type", "chrome"); + if (datafileVersion == 4) { + testUserAttributes.put(ATTRIBUTE_HOUSE_KEY, AUDIENCE_GRYFFINDOR_VALUE); + } + else { + testUserAttributes.put("browser_type", "chrome"); + } testUserAttributes.put("unknownAttribute", "dimValue"); Map testParams = new HashMap(); @@ -418,7 +461,8 @@ public void activateWithUnknownAttribute() throws Exception { when(mockBucketer.bucket(activatedExperiment, "userId")) .thenReturn(bucketedVariation); - logbackVerifier.expectMessage(Level.INFO, "Activating user \"userId\" in experiment \"etag1\"."); + logbackVerifier.expectMessage(Level.INFO, "Activating user \"userId\" in experiment \"" + + activatedExperiment.getKey() + "\"."); logbackVerifier.expectMessage(Level.WARN, "Attribute(s) [unknownAttribute] not in the datafile."); logbackVerifier.expectMessage(Level.DEBUG, "Dispatching impression event to URL test_url with params " + testParams + " and payload \"\""); @@ -552,16 +596,24 @@ public void activateWithNullAttributeValues() throws Exception { */ @Test public void activateDraftExperiment() throws Exception { - Experiment draftExperiment = validProjectConfig.getExperiments().get(1); + Experiment inactiveExperiment; + if (datafileVersion == 4) { + inactiveExperiment = validProjectConfig.getExperimentKeyMapping().get(EXPERIMENT_PAUSED_EXPERIMENT_KEY); + } + else { + inactiveExperiment = validProjectConfig.getExperiments().get(1); + } Optimizely optimizely = Optimizely.builder(validDatafile, mockEventHandler) .withConfig(validProjectConfig) .build(); - logbackVerifier.expectMessage(Level.INFO, "Experiment \"etag2\" is not running."); - logbackVerifier.expectMessage(Level.INFO, "Not activating user \"userId\" for experiment \"etag2\"."); + logbackVerifier.expectMessage(Level.INFO, "Experiment \"" + inactiveExperiment.getKey() + + "\" is not running."); + logbackVerifier.expectMessage(Level.INFO, "Not activating user \"userId\" for experiment \"" + + inactiveExperiment.getKey() + "\"."); - Variation variation = optimizely.activate(draftExperiment.getKey(), "userId"); + Variation variation = optimizely.activate(inactiveExperiment.getKey(), "userId"); // verify that null is returned, as the experiment isn't running assertNull(variation); @@ -591,7 +643,13 @@ public void activateUserInAudience() throws Exception { */ @Test public void activateUserNotInAudience() throws Exception { - Experiment experimentToCheck = validProjectConfig.getExperiments().get(0); + Experiment experimentToCheck; + if (datafileVersion == 4) { + experimentToCheck = validProjectConfig.getExperimentKeyMapping().get(EXPERIMENT_MULTIVARIATE_EXPERIMENT_KEY); + } + else { + experimentToCheck = validProjectConfig.getExperiments().get(0); + } Optimizely optimizely = Optimizely.builder(validDatafile, mockEventHandler) .withConfig(validProjectConfig) @@ -602,8 +660,10 @@ public void activateUserNotInAudience() throws Exception { testUserAttributes.put("browser_type", "firefox"); logbackVerifier.expectMessage(Level.INFO, - "User \"userId\" does not meet conditions to be in experiment \"etag1\"."); - logbackVerifier.expectMessage(Level.INFO, "Not activating user \"userId\" for experiment \"etag1\"."); + "User \"userId\" does not meet conditions to be in experiment \"" + + experimentToCheck.getKey() + "\"."); + logbackVerifier.expectMessage(Level.INFO, "Not activating user \"userId\" for experiment \"" + + experimentToCheck.getKey() + "\"."); Variation actualVariation = optimizely.activate(experimentToCheck.getKey(), "userId", testUserAttributes); assertNull(actualVariation); @@ -630,14 +690,21 @@ public void activateUserWithNoAudiences() throws Exception { */ @Test public void activateUserNoAttributesWithAudiences() throws Exception { - Experiment experiment = validProjectConfig.getExperiments().get(0); + Experiment experiment; + if (datafileVersion == 4) { + experiment = validProjectConfig.getExperimentKeyMapping().get(EXPERIMENT_MULTIVARIATE_EXPERIMENT_KEY); + } + else { + experiment = validProjectConfig.getExperiments().get(0); + } Optimizely optimizely = Optimizely.builder(validDatafile, mockEventHandler) .build(); logbackVerifier.expectMessage(Level.INFO, - "User \"userId\" does not meet conditions to be in experiment \"etag1\"."); - logbackVerifier.expectMessage(Level.INFO, "Not activating user \"userId\" for experiment \"etag1\"."); + "User \"userId\" does not meet conditions to be in experiment \"" + experiment.getKey() + "\"."); + logbackVerifier.expectMessage(Level.INFO, "Not activating user \"userId\" for experiment \"" + + experiment.getKey() + "\"."); assertNull(optimizely.activate(experiment.getKey(), "userId")); } @@ -672,6 +739,14 @@ public void activateForGroupExperimentWithMatchingAttributes() throws Exception .get(0); Variation variation = experiment.getVariations().get(0); + Map attributes = new HashMap(); + if (datafileVersion == 4) { + attributes.put(ATTRIBUTE_HOUSE_KEY, AUDIENCE_GRYFFINDOR_VALUE); + } + else { + attributes.put("browser_type", "chrome"); + } + when(mockBucketer.bucket(experiment, "user")).thenReturn(variation); Optimizely optimizely = Optimizely.builder(validDatafile, mockEventHandler) @@ -679,7 +754,7 @@ public void activateForGroupExperimentWithMatchingAttributes() throws Exception .withBucketing(mockBucketer) .build(); - assertThat(optimizely.activate(experiment.getKey(), "user", Collections.singletonMap("browser_type", "chrome")), + assertThat(optimizely.activate(experiment.getKey(), "user", attributes), is(variation)); } @@ -714,16 +789,29 @@ public void activateForGroupExperimentWithNonMatchingAttributes() throws Excepti */ @Test public void activateForcedVariationPrecedesAudienceEval() throws Exception { - Experiment experiment = validProjectConfig.getExperiments().get(0); - Variation expectedVariation = experiment.getVariations().get(0); + Experiment experiment; + String whitelistedUserId; + Variation expectedVariation; + if (datafileVersion == 4) { + experiment = validProjectConfig.getExperimentKeyMapping().get(EXPERIMENT_MULTIVARIATE_EXPERIMENT_KEY); + whitelistedUserId = MULTIVARIATE_EXPERIMENT_FORCED_VARIATION_USER_ID_GRED; + expectedVariation = experiment.getVariationKeyToVariationMap().get(VARIATION_MULTIVARIATE_EXPERIMENT_GRED_KEY); + } + else { + experiment = validProjectConfig.getExperiments().get(0); + whitelistedUserId = "testUser1"; + expectedVariation = experiment.getVariations().get(0); + } Optimizely optimizely = Optimizely.builder(validDatafile, mockEventHandler) .withConfig(validProjectConfig) .build(); - logbackVerifier.expectMessage(Level.INFO, "User \"testUser1\" is forced in variation \"vtag1\"."); + logbackVerifier.expectMessage(Level.INFO, "User \"" + whitelistedUserId + "\" is forced in variation \"" + + expectedVariation.getKey() + "\"."); // no attributes provided for a experiment that has an audience - assertThat(optimizely.activate(experiment.getKey(), "testUser1"), is(expectedVariation)); + assertTrue(experiment.getUserIdToVariationKeyMap().containsKey(whitelistedUserId)); + assertThat(optimizely.activate(experiment.getKey(), whitelistedUserId), is(expectedVariation)); } /** @@ -732,16 +820,27 @@ public void activateForcedVariationPrecedesAudienceEval() throws Exception { */ @Test public void activateExperimentStatusPrecedesForcedVariation() throws Exception { - Experiment experiment = validProjectConfig.getExperiments().get(1); + Experiment experiment; + String whitelistedUserId; + if (datafileVersion == 4) { + experiment = validProjectConfig.getExperimentKeyMapping().get(EXPERIMENT_PAUSED_EXPERIMENT_KEY); + whitelistedUserId = PAUSED_EXPERIMENT_FORCED_VARIATION_USER_ID_CONTROL; + } + else { + experiment = validProjectConfig.getExperiments().get(1); + whitelistedUserId = "testUser3"; + } Optimizely optimizely = Optimizely.builder(validDatafile, mockEventHandler) .withConfig(validProjectConfig) .build(); - logbackVerifier.expectMessage(Level.INFO, "Experiment \"etag2\" is not running."); - logbackVerifier.expectMessage(Level.INFO, "Not activating user \"testUser3\" for experiment \"etag2\"."); + logbackVerifier.expectMessage(Level.INFO, "Experiment \"" + experiment.getKey() + "\" is not running."); + logbackVerifier.expectMessage(Level.INFO, "Not activating user \"" + whitelistedUserId + + "\" for experiment \"" + experiment.getKey() + "\"."); // testUser3 has a corresponding forced variation, but experiment status should be checked first - assertNull(optimizely.activate(experiment.getKey(), "testUser3")); + assertTrue(experiment.getUserIdToVariationKeyMap().containsKey(whitelistedUserId)); + assertNull(optimizely.activate(experiment.getKey(), whitelistedUserId)); } /** @@ -768,7 +867,13 @@ public void activateDispatchEventThrowsException() throws Exception { */ @Test public void activateLaunchedExperimentDoesNotDispatchEvent() throws Exception { - Experiment launchedExperiment = noAudienceProjectConfig.getExperiments().get(2); + Experiment launchedExperiment; + if (datafileVersion == 4) { + launchedExperiment = validProjectConfig.getExperimentKeyMapping().get(EXPERIMENT_LAUNCHED_EXPERIMENT_KEY); + } + else { + launchedExperiment = noAudienceProjectConfig.getExperiments().get(2); + } Optimizely optimizely = Optimizely.builder(noAudienceDatafile, mockEventHandler) .withBucketing(mockBucketer) @@ -799,13 +904,29 @@ public void activateLaunchedExperimentDoesNotDispatchEvent() throws Exception { */ @Test public void trackEventEndToEnd() throws Exception { - List allExperiments = noAudienceProjectConfig.getExperiments(); - EventType eventType = noAudienceProjectConfig.getEventTypes().get(0); + EventType eventType; + String datafile; + ProjectConfig config; + if (datafileVersion == 4) { + config = validProjectConfig; + eventType = validProjectConfig.getEventNameMapping().get(EVENT_BASIC_EVENT_KEY); + datafile = validDatafile; + } + else { + config = noAudienceProjectConfig; + eventType = noAudienceProjectConfig.getEventTypes().get(0); + datafile = noAudienceDatafile; + } + List allExperiments = config.getExperiments(); EventBuilder eventBuilderV2 = new EventBuilderV2(); + DecisionService spyDecisionService = spy(new DecisionService(mockBucketer, + mockErrorHandler, + validProjectConfig, + null)); - Optimizely optimizely = Optimizely.builder(noAudienceDatafile, mockEventHandler) - .withBucketing(mockBucketer) + Optimizely optimizely = Optimizely.builder(datafile, mockEventHandler) + .withDecisionService(spyDecisionService) .withEventBuilder(eventBuilderV2) .withConfig(noAudienceProjectConfig) .withErrorHandler(mockErrorHandler) @@ -822,13 +943,14 @@ public void trackEventEndToEnd() throws Exception { optimizely.track(eventType.getKey(), "userId"); // verify that the bucketing algorithm was called only on experiments corresponding to the specified goal. - List experimentsForEvent = noAudienceProjectConfig.getExperimentsForEventKey(eventType.getKey()); + List experimentsForEvent = config.getExperimentsForEventKey(eventType.getKey()); for (Experiment experiment : allExperiments) { - if (ExperimentUtils.isExperimentActive(experiment) && - experimentsForEvent.contains(experiment)) { - verify(mockBucketer).bucket(experiment, "userId"); + if (experiment.isRunning() && experimentsForEvent.contains(experiment)) { + verify(spyDecisionService).getVariation(experiment, "userId", + Collections.emptyMap()); } else { - verify(mockBucketer, never()).bucket(experiment, "userId"); + verify(spyDecisionService, never()).getVariation(experiment, "userId", + Collections.emptyMap()); } } @@ -1071,7 +1193,7 @@ public void trackEventWithNullAttributeValues() throws Exception { } /** - * Verify that {@link Optimizely#track(String, String)} handles the case where an unknown attribute + * Verify that {@link Optimizely#track(String, String, Map)} handles the case where an unknown attribute * (i.e., not in the config) is passed through. * * In this case, the track event call should remove the unknown attribute from the given map. @@ -1551,14 +1673,20 @@ public void getVariationWithAudiences() throws Exception { */ @Test public void getVariationWithAudiencesNoAttributes() throws Exception { - Experiment experiment = validProjectConfig.getExperiments().get(0); + Experiment experiment; + if (datafileVersion == 4) { + experiment = validProjectConfig.getExperimentKeyMapping().get(EXPERIMENT_MULTIVARIATE_EXPERIMENT_KEY); + } + else { + experiment = validProjectConfig.getExperiments().get(0); + } Optimizely optimizely = Optimizely.builder(validDatafile, mockEventHandler) .withErrorHandler(mockErrorHandler) .build(); logbackVerifier.expectMessage(Level.INFO, - "User \"userId\" does not meet conditions to be in experiment \"etag1\"."); + "User \"userId\" does not meet conditions to be in experiment \"" + experiment.getKey() + "\"."); Variation actualVariation = optimizely.getVariation(experiment.getKey(), "userId"); assertNull(actualVariation); @@ -1671,13 +1799,19 @@ public void getVariationForGroupExperimentWithNonMatchingAttributes() throws Exc */ @Test public void getVariationExperimentStatusPrecedesForcedVariation() throws Exception { - Experiment experiment = validProjectConfig.getExperiments().get(1); + Experiment experiment; + if (datafileVersion == 4) { + experiment = validProjectConfig.getExperimentKeyMapping().get(EXPERIMENT_PAUSED_EXPERIMENT_KEY); + } + else { + experiment = validProjectConfig.getExperiments().get(1); + } Optimizely optimizely = Optimizely.builder(validDatafile, mockEventHandler) .withConfig(validProjectConfig) .build(); - logbackVerifier.expectMessage(Level.INFO, "Experiment \"etag2\" is not running."); + logbackVerifier.expectMessage(Level.INFO, "Experiment \"" + experiment.getKey() + "\" is not running."); // testUser3 has a corresponding forced variation, but experiment status should be checked first assertNull(optimizely.getVariation(experiment.getKey(), "testUser3")); } @@ -1702,7 +1836,13 @@ public void addNotificationListener() throws Exception { .withErrorHandler(mockErrorHandler) .build(); - Map attributes = Collections.singletonMap("browser_type", "chrome"); + Map attributes = new HashMap(); + if (datafileVersion == 4) { + attributes.put(ATTRIBUTE_HOUSE_KEY, AUDIENCE_GRYFFINDOR_VALUE); + } + else { + attributes.put("browser_type", "chrome"); + } Map testParams = new HashMap(); testParams.put("test", "params"); @@ -1725,10 +1865,8 @@ public void addNotificationListener() throws Exception { // Check if listener is notified when experiment is activated Variation actualVariation = optimizely.activate(activatedExperiment, genericUserId, attributes); - if (datafileVersion == 3 || datafileVersion == 2) { - verify(listener, times(1)) - .onExperimentActivated(activatedExperiment, genericUserId, attributes, actualVariation); - } + verify(listener, times(1)) + .onExperimentActivated(activatedExperiment, genericUserId, attributes, actualVariation); // Check if listener is notified after an event is tracked EventType eventType = validProjectConfig.getEventTypes().get(0); @@ -1845,7 +1983,13 @@ public void clearNotificationListeners() throws Exception { .withErrorHandler(mockErrorHandler) .build(); - Map attributes = Collections.singletonMap("browser_type", "chrome"); + Map attributes = new HashMap(); + if (datafileVersion == 4) { + attributes.put(ATTRIBUTE_HOUSE_KEY, AUDIENCE_GRYFFINDOR_VALUE); + } + else { + attributes.put("browser_type", "chrome"); + } Map testParams = new HashMap(); testParams.put("test", "params"); @@ -1857,9 +2001,16 @@ public void clearNotificationListeners() throws Exception { when(mockBucketer.bucket(activatedExperiment, genericUserId)) .thenReturn(bucketedVariation); - when(mockEventBuilder.createImpressionEvent(validProjectConfig, activatedExperiment, bucketedVariation, genericUserId, - attributes)) - .thenReturn(logEventToDispatch); + // set up argument captor for the attributes map to compare map equality + ArgumentCaptor attributeCaptor = ArgumentCaptor.forClass(Map.class); + + when(mockEventBuilder.createImpressionEvent( + eq(validProjectConfig), + eq(activatedExperiment), + eq(bucketedVariation), + eq(genericUserId), + attributeCaptor.capture() + )).thenReturn(logEventToDispatch); NotificationListener listener = mock(NotificationListener.class); optimizely.addNotificationListener(listener); @@ -1867,6 +2018,9 @@ public void clearNotificationListeners() throws Exception { // Check if listener is notified after an experiment is activated Variation actualVariation = optimizely.activate(activatedExperiment, genericUserId, attributes); + // check that the argument that was captured by the mockEventBuilder attribute captor, + // was equal to the attributes passed in to activate + assertEquals(attributes, attributeCaptor.getValue()); verify(listener, never()) .onExperimentActivated(activatedExperiment, genericUserId, attributes, actualVariation); From 1a768200fe1ed9041e0ce92f10875fc638299823 Mon Sep 17 00:00:00 2001 From: wangjoshuah Date: Thu, 6 Jul 2017 12:22:12 -0700 Subject: [PATCH 2/2] update eventbuilder v2 tests and experiment mapping generation helper --- .../com/optimizely/ab/OptimizelyTest.java | 237 ++++++++++++------ .../ab/event/internal/EventBuilderV2Test.java | 185 ++++++++++---- 2 files changed, 297 insertions(+), 125 deletions(-) diff --git a/core-api/src/test/java/com/optimizely/ab/OptimizelyTest.java b/core-api/src/test/java/com/optimizely/ab/OptimizelyTest.java index aa26faad2..4ad647457 100644 --- a/core-api/src/test/java/com/optimizely/ab/OptimizelyTest.java +++ b/core-api/src/test/java/com/optimizely/ab/OptimizelyTest.java @@ -34,7 +34,6 @@ import com.optimizely.ab.event.LogEvent; import com.optimizely.ab.event.internal.EventBuilder; import com.optimizely.ab.event.internal.EventBuilderV2; -import com.optimizely.ab.internal.ExperimentUtils; import com.optimizely.ab.internal.LogbackVerifier; import com.optimizely.ab.notification.NotificationListener; import edu.umd.cs.findbugs.annotations.SuppressFBWarnings; @@ -50,8 +49,6 @@ import org.mockito.junit.MockitoRule; import java.io.IOException; -import java.nio.file.ProviderNotFoundException; -import java.util.ArrayList; import java.util.Arrays; import java.util.Collection; import java.util.Collections; @@ -72,6 +69,8 @@ import static com.optimizely.ab.config.ValidProjectConfigV4.ATTRIBUTE_HOUSE_KEY; import static com.optimizely.ab.config.ValidProjectConfigV4.AUDIENCE_GRYFFINDOR_VALUE; import static com.optimizely.ab.config.ValidProjectConfigV4.EVENT_BASIC_EVENT_KEY; +import static com.optimizely.ab.config.ValidProjectConfigV4.EVENT_LAUNCHED_EXPERIMENT_ONLY_KEY; +import static com.optimizely.ab.config.ValidProjectConfigV4.EXPERIMENT_BASIC_EXPERIMENT_KEY; import static com.optimizely.ab.config.ValidProjectConfigV4.EXPERIMENT_LAUNCHED_EXPERIMENT_KEY; import static com.optimizely.ab.config.ValidProjectConfigV4.EXPERIMENT_MULTIVARIATE_EXPERIMENT_KEY; import static com.optimizely.ab.config.ValidProjectConfigV4.EXPERIMENT_PAUSED_EXPERIMENT_KEY; @@ -81,15 +80,18 @@ import static com.optimizely.ab.event.LogEvent.RequestMethod; import static com.optimizely.ab.event.internal.EventBuilderV2Test.createExperimentVariationMap; import static java.util.Arrays.asList; +import static junit.framework.TestCase.assertTrue; import static org.hamcrest.CoreMatchers.is; import static org.hamcrest.CoreMatchers.not; import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.array; import static org.hamcrest.Matchers.hasEntry; import static org.hamcrest.Matchers.hasKey; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertNotNull; import static org.junit.Assert.assertNull; import static org.mockito.Matchers.any; +import static org.mockito.Matchers.anyMap; import static org.mockito.Matchers.anyMapOf; import static org.mockito.Matchers.anyString; import static org.mockito.Matchers.eq; @@ -1005,7 +1007,13 @@ public void trackEventWithUnknownEventKeyAndRaiseExceptionErrorHandler() throws @SuppressWarnings("unchecked") public void trackEventWithAttributes() throws Exception { Attribute attribute = validProjectConfig.getAttributes().get(0); - EventType eventType = validProjectConfig.getEventTypes().get(0); + EventType eventType; + if (datafileVersion == 4) { + eventType = validProjectConfig.getEventNameMapping().get(EVENT_BASIC_EVENT_KEY); + } + else { + eventType = validProjectConfig.getEventTypes().get(0); + } // setup a mock event builder to return expected conversion params EventBuilder mockEventBuilder = mock(EventBuilder.class); @@ -1022,7 +1030,7 @@ public void trackEventWithAttributes() throws Exception { Map attributes = ImmutableMap.of(attribute.getKey(), "attributeValue"); Map experimentVariationMap = createExperimentVariationMap( validProjectConfig, - mockBucketer, + mockDecisionService, eventType.getKey(), genericUserId, attributes); @@ -1037,7 +1045,8 @@ public void trackEventWithAttributes() throws Exception { eq(Collections.emptyMap()))) .thenReturn(logEventToDispatch); - logbackVerifier.expectMessage(Level.INFO, "Tracking event \"clicked_cart\" for user \"" + genericUserId + "\"."); + logbackVerifier.expectMessage(Level.INFO, "Tracking event \"" + eventType.getKey() + + "\" for user \"" + genericUserId + "\"."); logbackVerifier.expectMessage(Level.DEBUG, "Dispatching conversion event to URL test_url with params " + testParams + " and payload \"\""); @@ -1071,29 +1080,35 @@ public void trackEventWithAttributes() throws Exception { value="NP_NONNULL_PARAM_VIOLATION", justification="testing nullness contract violation") public void trackEventWithNullAttributes() throws Exception { - EventType eventType = noAudienceProjectConfig.getEventTypes().get(0); + EventType eventType; + if (datafileVersion == 4) { + eventType = validProjectConfig.getEventNameMapping().get(EVENT_BASIC_EVENT_KEY); + } + else { + eventType = validProjectConfig.getEventTypes().get(0); + } // setup a mock event builder to return expected conversion params EventBuilder mockEventBuilder = mock(EventBuilder.class); - Optimizely optimizely = Optimizely.builder(noAudienceDatafile, mockEventHandler) + Optimizely optimizely = Optimizely.builder(validDatafile, mockEventHandler) .withBucketing(mockBucketer) .withEventBuilder(mockEventBuilder) - .withConfig(noAudienceProjectConfig) + .withConfig(validProjectConfig) .withErrorHandler(mockErrorHandler) .build(); Map testParams = new HashMap(); testParams.put("test", "params"); Map experimentVariationMap = createExperimentVariationMap( - noAudienceProjectConfig, - mockBucketer, + validProjectConfig, + mockDecisionService, eventType.getKey(), genericUserId, Collections.emptyMap()); LogEvent logEventToDispatch = new LogEvent(RequestMethod.GET, "test_url", testParams, ""); when(mockEventBuilder.createConversionEvent( - eq(noAudienceProjectConfig), + eq(validProjectConfig), eq(experimentVariationMap), eq(genericUserId), eq(eventType.getId()), @@ -1102,7 +1117,8 @@ public void trackEventWithNullAttributes() throws Exception { eq(Collections.emptyMap()))) .thenReturn(logEventToDispatch); - logbackVerifier.expectMessage(Level.INFO, "Tracking event \"clicked_cart\" for user \"" + genericUserId + "\"."); + logbackVerifier.expectMessage(Level.INFO, "Tracking event \"" + eventType.getKey() + + "\" for user \"" + genericUserId + "\"."); logbackVerifier.expectMessage(Level.DEBUG, "Dispatching conversion event to URL test_url with params " + testParams + " and payload \"\""); @@ -1117,7 +1133,7 @@ public void trackEventWithNullAttributes() throws Exception { // verify that the event builder was called with the expected attributes verify(mockEventBuilder).createConversionEvent( - eq(noAudienceProjectConfig), + eq(validProjectConfig), eq(experimentVariationMap), eq(genericUserId), eq(eventType.getId()), @@ -1136,29 +1152,35 @@ public void trackEventWithNullAttributes() throws Exception { */ @Test public void trackEventWithNullAttributeValues() throws Exception { - EventType eventType = noAudienceProjectConfig.getEventTypes().get(0); + EventType eventType; + if (datafileVersion == 4) { + eventType = validProjectConfig.getEventNameMapping().get(EVENT_BASIC_EVENT_KEY); + } + else { + eventType = validProjectConfig.getEventTypes().get(0); + } // setup a mock event builder to return expected conversion params EventBuilder mockEventBuilder = mock(EventBuilder.class); - Optimizely optimizely = Optimizely.builder(noAudienceDatafile, mockEventHandler) + Optimizely optimizely = Optimizely.builder(validDatafile, mockEventHandler) .withBucketing(mockBucketer) .withEventBuilder(mockEventBuilder) - .withConfig(noAudienceProjectConfig) + .withConfig(validProjectConfig) .withErrorHandler(mockErrorHandler) .build(); Map testParams = new HashMap(); testParams.put("test", "params"); Map experimentVariationMap = createExperimentVariationMap( - noAudienceProjectConfig, - mockBucketer, + validProjectConfig, + mockDecisionService, eventType.getKey(), genericUserId, Collections.emptyMap()); LogEvent logEventToDispatch = new LogEvent(RequestMethod.GET, "test_url", testParams, ""); when(mockEventBuilder.createConversionEvent( - eq(noAudienceProjectConfig), + eq(validProjectConfig), eq(experimentVariationMap), eq(genericUserId), eq(eventType.getId()), @@ -1167,7 +1189,8 @@ public void trackEventWithNullAttributeValues() throws Exception { eq(Collections.emptyMap()))) .thenReturn(logEventToDispatch); - logbackVerifier.expectMessage(Level.INFO, "Tracking event \"clicked_cart\" for user \"" + genericUserId + "\"."); + logbackVerifier.expectMessage(Level.INFO, "Tracking event \"" + eventType.getKey() + + "\" for user \"" + genericUserId + "\"."); logbackVerifier.expectMessage(Level.DEBUG, "Dispatching conversion event to URL test_url with params " + testParams + " and payload \"\""); @@ -1181,7 +1204,7 @@ public void trackEventWithNullAttributeValues() throws Exception { // verify that the event builder was called with the expected attributes verify(mockEventBuilder).createConversionEvent( - eq(noAudienceProjectConfig), + eq(validProjectConfig), eq(experimentVariationMap), eq(genericUserId), eq(eventType.getId()), @@ -1193,7 +1216,7 @@ public void trackEventWithNullAttributeValues() throws Exception { } /** - * Verify that {@link Optimizely#track(String, String, Map)} handles the case where an unknown attribute + * Verify that {@link Optimizely#track(String, String)} handles the case where an unknown attribute * (i.e., not in the config) is passed through. * * In this case, the track event call should remove the unknown attribute from the given map. @@ -1201,7 +1224,13 @@ public void trackEventWithNullAttributeValues() throws Exception { @Test @SuppressWarnings("unchecked") public void trackEventWithUnknownAttribute() throws Exception { - EventType eventType = validProjectConfig.getEventTypes().get(0); + EventType eventType; + if (datafileVersion == 4) { + eventType = validProjectConfig.getEventNameMapping().get(EVENT_BASIC_EVENT_KEY); + } + else { + eventType = validProjectConfig.getEventTypes().get(0); + } // setup a mock event builder to return expected conversion params EventBuilder mockEventBuilder = mock(EventBuilder.class); @@ -1217,7 +1246,7 @@ public void trackEventWithUnknownAttribute() throws Exception { testParams.put("test", "params"); Map experimentVariationMap = createExperimentVariationMap( validProjectConfig, - mockBucketer, + mockDecisionService, eventType.getKey(), genericUserId, Collections.emptyMap()); @@ -1232,7 +1261,8 @@ public void trackEventWithUnknownAttribute() throws Exception { eq(Collections.emptyMap()))) .thenReturn(logEventToDispatch); - logbackVerifier.expectMessage(Level.INFO, "Tracking event \"clicked_cart\" for user \"" + genericUserId + "\"."); + logbackVerifier.expectMessage(Level.INFO, "Tracking event \"" + eventType.getKey() + + "\" for user \"" + genericUserId + "\"."); logbackVerifier.expectMessage(Level.WARN, "Attribute(s) [unknownAttribute] not in the datafile."); logbackVerifier.expectMessage(Level.DEBUG, "Dispatching conversion event to URL test_url with params " + testParams + " and payload \"\""); @@ -1265,7 +1295,13 @@ public void trackEventWithUnknownAttribute() throws Exception { */ @Test public void trackEventWithEventTags() throws Exception { - EventType eventType = validProjectConfig.getEventTypes().get(0); + EventType eventType; + if (datafileVersion == 4) { + eventType = validProjectConfig.getEventNameMapping().get(EVENT_BASIC_EVENT_KEY); + } + else { + eventType = validProjectConfig.getEventTypes().get(0); + } // setup a mock event builder to return expected conversion params EventBuilder mockEventBuilder = mock(EventBuilder.class); @@ -1287,7 +1323,7 @@ public void trackEventWithEventTags() throws Exception { eventTags.put("float_param", 12.3f); Map experimentVariationMap = createExperimentVariationMap( validProjectConfig, - mockBucketer, + mockDecisionService, eventType.getKey(), genericUserId, Collections.emptyMap()); @@ -1303,7 +1339,8 @@ public void trackEventWithEventTags() throws Exception { eq(eventTags))) .thenReturn(logEventToDispatch); - logbackVerifier.expectMessage(Level.INFO, "Tracking event \"clicked_cart\" for user \"" + genericUserId + "\"."); + logbackVerifier.expectMessage(Level.INFO, "Tracking event \"" + eventType.getKey() + "\" for user \"" + + genericUserId + "\"."); logbackVerifier.expectMessage(Level.DEBUG, "Dispatching conversion event to URL test_url with params " + testParams + " and payload \"\""); @@ -1341,7 +1378,13 @@ public void trackEventWithEventTags() throws Exception { value="NP_NONNULL_PARAM_VIOLATION", justification="testing nullness contract violation") public void trackEventWithNullEventTags() throws Exception { - EventType eventType = validProjectConfig.getEventTypes().get(0); + EventType eventType; + if (datafileVersion == 4) { + eventType = validProjectConfig.getEventNameMapping().get(EVENT_BASIC_EVENT_KEY); + } + else { + eventType = validProjectConfig.getEventTypes().get(0); + } // setup a mock event builder to return expected conversion params EventBuilder mockEventBuilder = mock(EventBuilder.class); @@ -1357,7 +1400,7 @@ public void trackEventWithNullEventTags() throws Exception { testParams.put("test", "params"); Map experimentVariationMap = createExperimentVariationMap( validProjectConfig, - mockBucketer, + mockDecisionService, eventType.getKey(), genericUserId, Collections.emptyMap()); @@ -1372,7 +1415,8 @@ public void trackEventWithNullEventTags() throws Exception { eq(Collections.emptyMap()))) .thenReturn(logEventToDispatch); - logbackVerifier.expectMessage(Level.INFO, "Tracking event \"clicked_cart\" for user \"" + genericUserId + "\"."); + logbackVerifier.expectMessage(Level.INFO, "Tracking event \"" + eventType.getKey() + + "\" for user \"" + genericUserId + "\"."); logbackVerifier.expectMessage(Level.DEBUG, "Dispatching conversion event to URL test_url with params " + testParams + " and payload \"\""); @@ -1398,16 +1442,28 @@ public void trackEventWithNullEventTags() throws Exception { */ @Test public void trackEventWithNoValidExperiments() throws Exception { + EventType eventType; + if (datafileVersion == 4) { + eventType = validProjectConfig.getEventNameMapping().get(EVENT_BASIC_EVENT_KEY); + } + else { + eventType = validProjectConfig.getEventNameMapping().get("clicked_purchase"); + } - Optimizely optimizely = Optimizely.builder(validDatafile, mockEventHandler).build(); + when(mockDecisionService.getVariation(any(Experiment.class), any(String.class), anyMapOf(String.class, String.class))) + .thenReturn(null); + Optimizely optimizely = Optimizely.builder(validDatafile, mockEventHandler) + .withDecisionService(mockDecisionService) + .build(); Map attributes = new HashMap(); attributes.put("browser_type", "firefox"); logbackVerifier.expectMessage(Level.INFO, - "There are no valid experiments for event \"clicked_purchase\" to track."); - logbackVerifier.expectMessage(Level.INFO, "Not tracking event \"clicked_purchase\" for user \"userId\"."); - optimizely.track("clicked_purchase", "userId", attributes); + "There are no valid experiments for event \"" + eventType.getKey() + "\" to track."); + logbackVerifier.expectMessage(Level.INFO, "Not tracking event \"" + eventType.getKey() + + "\" for user \"userId\"."); + optimizely.track(eventType.getKey(), "userId", attributes); verify(mockEventHandler, never()).dispatchEvent(any(LogEvent.class)); } @@ -1436,7 +1492,13 @@ public void trackDispatchEventThrowsException() throws Exception { */ @Test public void trackDoesNotSendEventWhenExperimentsAreLaunchedOnly() throws Exception { - EventType eventType = noAudienceProjectConfig.getEventNameMapping().get("launched_exp_event"); + EventType eventType; + if (datafileVersion == 4) { + eventType = validProjectConfig.getEventNameMapping().get(EVENT_LAUNCHED_EXPERIMENT_ONLY_KEY); + } + else { + eventType = noAudienceProjectConfig.getEventNameMapping().get("launched_exp_event"); + } Bucketer mockBucketAlgorithm = mock(Bucketer.class); for (Experiment experiment : noAudienceProjectConfig.getExperiments()) { Variation variation = experiment.getVariations().get(0); @@ -1487,14 +1549,20 @@ public void trackDoesNotSendEventWhenExperimentsAreLaunchedOnly() throws Excepti @Test public void trackDispatchesWhenEventHasLaunchedAndRunningExperiments() throws Exception { EventBuilder mockEventBuilder = mock(EventBuilder.class); - EventType eventType = noAudienceProjectConfig.getEventNameMapping().get("event_with_launched_and_running_experiments"); + EventType eventType; + if (datafileVersion == 4) { + eventType = validProjectConfig.getEventNameMapping().get(EVENT_BASIC_EVENT_KEY); + } + else { + eventType = noAudienceProjectConfig.getEventNameMapping().get("event_with_launched_and_running_experiments"); + } Bucketer mockBucketAlgorithm = mock(Bucketer.class); - for (Experiment experiment : noAudienceProjectConfig.getExperiments()) { + for (Experiment experiment : validProjectConfig.getExperiments()) { when(mockBucketAlgorithm.bucket(experiment, genericUserId)) .thenReturn(experiment.getVariations().get(0)); } - Optimizely client = Optimizely.builder(noAudienceDatafile, mockEventHandler) + Optimizely client = Optimizely.builder(validDatafile, mockEventHandler) .withConfig(noAudienceProjectConfig) .withBucketing(mockBucketAlgorithm) .withEventBuilder(mockEventBuilder) @@ -1504,14 +1572,18 @@ public void trackDispatchesWhenEventHasLaunchedAndRunningExperiments() throws Ex testParams.put("test", "params"); Map experimentVariationMap = createExperimentVariationMap( noAudienceProjectConfig, - mockBucketAlgorithm, + client.decisionService, eventType.getKey(), genericUserId, - null); + Collections.emptyMap()); + + // Create an Argument Captor to ensure we are creating a correct experiment variation map + ArgumentCaptor experimentVariationMapCaptor = ArgumentCaptor.forClass(Map.class); + LogEvent conversionEvent = new LogEvent(RequestMethod.GET, "test_url", testParams, ""); when(mockEventBuilder.createConversionEvent( eq(noAudienceProjectConfig), - eq(experimentVariationMap), + experimentVariationMapCaptor.capture(), eq(genericUserId), eq(eventType.getId()), eq(eventType.getKey()), @@ -1534,6 +1606,10 @@ public void trackDispatchesWhenEventHasLaunchedAndRunningExperiments() throws Ex // It should send a track event with the running experiment client.track(eventType.getKey(), genericUserId, Collections.emptyMap()); verify(client.eventHandler).dispatchEvent(eq(conversionEvent)); + + // Check the argument captor got the correct arguments + Map actualExperimentVariationMap = experimentVariationMapCaptor.getValue(); + assertEquals(experimentVariationMap, actualExperimentVariationMap); } /** @@ -1763,6 +1839,14 @@ public void getVariationForGroupExperimentWithMatchingAttributes() throws Except .get(0); Variation variation = experiment.getVariations().get(0); + Map attributes = new HashMap(); + if (datafileVersion == 4) { + attributes.put(ATTRIBUTE_HOUSE_KEY, AUDIENCE_GRYFFINDOR_VALUE); + } + else { + attributes.put("browser_type", "chrome"); + } + when(mockBucketer.bucket(experiment, "user")).thenReturn(variation); Optimizely optimizely = Optimizely.builder(validDatafile, mockEventHandler) @@ -1770,7 +1854,7 @@ public void getVariationForGroupExperimentWithMatchingAttributes() throws Except .withBucketing(mockBucketer) .build(); - assertThat(optimizely.getVariation(experiment.getKey(), "user", Collections.singletonMap("browser_type", "chrome")), + assertThat(optimizely.getVariation(experiment.getKey(), "user", attributes), is(variation)); } @@ -1825,24 +1909,27 @@ public void getVariationExperimentStatusPrecedesForcedVariation() throws Excepti */ @Test public void addNotificationListener() throws Exception { - Experiment activatedExperiment = validProjectConfig.getExperiments().get(0); + Experiment activatedExperiment; + EventType eventType; + if (datafileVersion == 4) { + activatedExperiment = validProjectConfig.getExperimentKeyMapping().get(EXPERIMENT_BASIC_EXPERIMENT_KEY); + eventType = validProjectConfig.getEventNameMapping().get(EVENT_BASIC_EVENT_KEY); + } + else { + activatedExperiment = validProjectConfig.getExperiments().get(0); + eventType = validProjectConfig.getEventTypes().get(0); + } Variation bucketedVariation = activatedExperiment.getVariations().get(0); EventBuilder mockEventBuilder = mock(EventBuilder.class); Optimizely optimizely = Optimizely.builder(validDatafile, mockEventHandler) - .withBucketing(mockBucketer) + .withDecisionService(mockDecisionService) .withEventBuilder(mockEventBuilder) .withConfig(validProjectConfig) .withErrorHandler(mockErrorHandler) .build(); - Map attributes = new HashMap(); - if (datafileVersion == 4) { - attributes.put(ATTRIBUTE_HOUSE_KEY, AUDIENCE_GRYFFINDOR_VALUE); - } - else { - attributes.put("browser_type", "chrome"); - } + Map attributes = Collections.emptyMap(); Map testParams = new HashMap(); testParams.put("test", "params"); @@ -1851,30 +1938,27 @@ public void addNotificationListener() throws Exception { bucketedVariation, genericUserId, attributes)) .thenReturn(logEventToDispatch); - when(mockBucketer.bucket(activatedExperiment, genericUserId)) + when(mockDecisionService.getVariation( + eq(activatedExperiment), + eq(genericUserId), + eq(Collections.emptyMap()))) .thenReturn(bucketedVariation); - when(mockEventBuilder.createImpressionEvent(validProjectConfig, activatedExperiment, - bucketedVariation, genericUserId, attributes)) - .thenReturn(logEventToDispatch); - // Add listener NotificationListener listener = mock(NotificationListener.class); optimizely.addNotificationListener(listener); // Check if listener is notified when experiment is activated Variation actualVariation = optimizely.activate(activatedExperiment, genericUserId, attributes); - verify(listener, times(1)) .onExperimentActivated(activatedExperiment, genericUserId, attributes, actualVariation); // Check if listener is notified after an event is tracked - EventType eventType = validProjectConfig.getEventTypes().get(0); String eventKey = eventType.getKey(); Map experimentVariationMap = createExperimentVariationMap( validProjectConfig, - mockBucketer, + mockDecisionService, eventType.getKey(), genericUserId, attributes); @@ -1911,7 +1995,8 @@ public void removeNotificationListener() throws Exception { .withErrorHandler(mockErrorHandler) .build(); - Map attributes = Collections.singletonMap("browser_type", "chrome"); + Map attributes = new HashMap(); + attributes.put(ATTRIBUTE_HOUSE_KEY, AUDIENCE_GRYFFINDOR_VALUE); Map testParams = new HashMap(); testParams.put("test", "params"); @@ -1937,6 +2022,8 @@ public void removeNotificationListener() throws Exception { verify(listener, never()) .onExperimentActivated(activatedExperiment, genericUserId, attributes, actualVariation); + // Check if listener is notified after a live variable is accessed + boolean activateExperiment = true; verify(listener, never()) .onExperimentActivated(activatedExperiment, genericUserId, attributes, actualVariation); @@ -1946,7 +2033,7 @@ public void removeNotificationListener() throws Exception { Map experimentVariationMap = createExperimentVariationMap( validProjectConfig, - mockBucketer, + mockDecisionService, eventType.getKey(), genericUserId, attributes); @@ -1972,7 +2059,16 @@ public void removeNotificationListener() throws Exception { */ @Test public void clearNotificationListeners() throws Exception { - Experiment activatedExperiment = validProjectConfig.getExperiments().get(0); + Experiment activatedExperiment; + Map attributes = new HashMap(); + if (datafileVersion == 4) { + activatedExperiment = validProjectConfig.getExperimentKeyMapping().get(EXPERIMENT_MULTIVARIATE_EXPERIMENT_KEY); + attributes.put(ATTRIBUTE_HOUSE_KEY, AUDIENCE_GRYFFINDOR_VALUE); + } + else { + activatedExperiment = validProjectConfig.getExperiments().get(0); + attributes.put("browser_type", "chrome"); + } Variation bucketedVariation = activatedExperiment.getVariations().get(0); EventBuilder mockEventBuilder = mock(EventBuilder.class); @@ -1983,14 +2079,6 @@ public void clearNotificationListeners() throws Exception { .withErrorHandler(mockErrorHandler) .build(); - Map attributes = new HashMap(); - if (datafileVersion == 4) { - attributes.put(ATTRIBUTE_HOUSE_KEY, AUDIENCE_GRYFFINDOR_VALUE); - } - else { - attributes.put("browser_type", "chrome"); - } - Map testParams = new HashMap(); testParams.put("test", "params"); LogEvent logEventToDispatch = new LogEvent(RequestMethod.GET, "test_url", testParams, ""); @@ -2018,12 +2106,15 @@ public void clearNotificationListeners() throws Exception { // Check if listener is notified after an experiment is activated Variation actualVariation = optimizely.activate(activatedExperiment, genericUserId, attributes); + // check that the argument that was captured by the mockEventBuilder attribute captor, // was equal to the attributes passed in to activate assertEquals(attributes, attributeCaptor.getValue()); verify(listener, never()) .onExperimentActivated(activatedExperiment, genericUserId, attributes, actualVariation); + // Check if listener is notified after a live variable is accessed + boolean activateExperiment = true; verify(listener, never()) .onExperimentActivated(activatedExperiment, genericUserId, attributes, actualVariation); @@ -2033,7 +2124,7 @@ public void clearNotificationListeners() throws Exception { Map experimentVariationMap = createExperimentVariationMap( validProjectConfig, - mockBucketer, + mockDecisionService, eventType.getKey(), OptimizelyTest.genericUserId, attributes); diff --git a/core-api/src/test/java/com/optimizely/ab/event/internal/EventBuilderV2Test.java b/core-api/src/test/java/com/optimizely/ab/event/internal/EventBuilderV2Test.java index de5a7a379..446eed4cb 100644 --- a/core-api/src/test/java/com/optimizely/ab/event/internal/EventBuilderV2Test.java +++ b/core-api/src/test/java/com/optimizely/ab/event/internal/EventBuilderV2Test.java @@ -18,11 +18,15 @@ import com.google.gson.Gson; import com.optimizely.ab.bucketing.Bucketer; +import com.optimizely.ab.bucketing.DecisionService; +import com.optimizely.ab.bucketing.UserProfileService; import com.optimizely.ab.config.Attribute; import com.optimizely.ab.config.EventType; import com.optimizely.ab.config.Experiment; import com.optimizely.ab.config.ProjectConfig; import com.optimizely.ab.config.Variation; +import com.optimizely.ab.error.ErrorHandler; +import com.optimizely.ab.error.NoOpErrorHandler; import com.optimizely.ab.event.LogEvent; import com.optimizely.ab.event.internal.payload.Conversion; import com.optimizely.ab.event.internal.payload.Decision; @@ -31,22 +35,39 @@ import com.optimizely.ab.event.internal.payload.Feature; import com.optimizely.ab.event.internal.payload.Impression; import com.optimizely.ab.event.internal.payload.LayerState; -import com.optimizely.ab.internal.ExperimentUtils; import com.optimizely.ab.internal.ReservedEventKey; import org.junit.BeforeClass; import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.Parameterized; +import org.junit.runners.Parameterized.Parameters; import javax.annotation.Nullable; import java.io.IOException; import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collection; import java.util.Collections; import java.util.HashMap; import java.util.List; import java.util.Map; +import static com.optimizely.ab.config.ProjectConfigTestUtils.noAudienceProjectConfigJsonV2; +import static com.optimizely.ab.config.ProjectConfigTestUtils.noAudienceProjectConfigV2; +import static com.optimizely.ab.config.ProjectConfigTestUtils.validConfigJsonV2; +import static com.optimizely.ab.config.ProjectConfigTestUtils.validConfigJsonV4; import static com.optimizely.ab.config.ProjectConfigTestUtils.validProjectConfigV2; +import static com.optimizely.ab.config.ProjectConfigTestUtils.validProjectConfigV4; +import static com.optimizely.ab.config.ValidProjectConfigV4.AUDIENCE_GRYFFINDOR_VALUE; +import static com.optimizely.ab.config.ValidProjectConfigV4.EVENT_BASIC_EVENT_KEY; +import static com.optimizely.ab.config.ValidProjectConfigV4.EVENT_PAUSED_EXPERIMENT_KEY; +import static com.optimizely.ab.config.ValidProjectConfigV4.MULTIVARIATE_EXPERIMENT_FORCED_VARIATION_USER_ID_GRED; +import static com.optimizely.ab.config.ValidProjectConfigV4.PAUSED_EXPERIMENT_FORCED_VARIATION_USER_ID_CONTROL; +import static junit.framework.TestCase.assertNotNull; import static org.hamcrest.CoreMatchers.is; import static org.hamcrest.Matchers.closeTo; +import static org.hamcrest.Matchers.containsInAnyOrder; +import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertNotEquals; import static org.junit.Assert.assertNull; @@ -54,23 +75,41 @@ import static org.junit.Assert.assertTrue; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.never; +import static org.mockito.Mockito.spy; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; /** * Test for {@link EventBuilderV2} */ +@RunWith(Parameterized.class) public class EventBuilderV2Test { + @Parameters + public static Collection data() throws IOException { + return Arrays.asList(new Object[][] { + { + 2, + validProjectConfigV2() + }, + { + 4, + validProjectConfigV4() + } + }); + } + private Gson gson = new Gson(); private EventBuilderV2 builder = new EventBuilderV2(); private static String userId = "userId"; - private static ProjectConfig validProjectConfig; + private int datafileVersion; + private ProjectConfig validProjectConfig; - @BeforeClass - public static void setUp() throws IOException { - validProjectConfig = validProjectConfigV2(); + public EventBuilderV2Test(int datafileVersion, + ProjectConfig validProjectConfig) { + this.datafileVersion = datafileVersion; + this.validProjectConfig = validProjectConfig; } /** @@ -201,16 +240,22 @@ public void createConversionEvent() throws Exception { // call the bucket function. for (Experiment experiment : allExperiments) { when(mockBucketAlgorithm.bucket(experiment, userId)) - .thenReturn(experiment.getVariations().get(0)); + .thenReturn(experiment.getVariations().get(0)); } + DecisionService decisionService = new DecisionService( + mockBucketAlgorithm, + mock(ErrorHandler.class), + validProjectConfig, + mock(UserProfileService.class) + ); - Map attributeMap = Collections.singletonMap(attribute.getKey(), "value"); + Map attributeMap = Collections.singletonMap(attribute.getKey(), AUDIENCE_GRYFFINDOR_VALUE); Map eventTagMap = new HashMap(); eventTagMap.put("boolean_param", false); eventTagMap.put("string_param", "123"); Map experimentVariationMap = createExperimentVariationMap( validProjectConfig, - mockBucketAlgorithm, + decisionService, eventType.getKey(), userId, attributeMap); @@ -226,7 +271,7 @@ public void createConversionEvent() throws Exception { List expectedLayerStates = new ArrayList(); for (Experiment experiment : experimentsForEventKey) { - if (ExperimentUtils.isExperimentActive(experiment)) { + if (experiment.isRunning()) { LayerState layerState = new LayerState(experiment.getLayerId(), validProjectConfig.getRevision(), new Decision(experiment.getVariations().get(0).getId(), false, experiment.getId()), true); expectedLayerStates.add(layerState); @@ -240,12 +285,12 @@ public void createConversionEvent() throws Exception { // verify payload information assertThat(conversion.getVisitorId(), is(userId)); - assertThat((double)conversion.getTimestamp(), closeTo((double)System.currentTimeMillis(), 60.0)); + assertThat((double)conversion.getTimestamp(), closeTo((double)System.currentTimeMillis(), 120.0)); assertThat(conversion.getProjectId(), is(validProjectConfig.getProjectId())); assertThat(conversion.getAccountId(), is(validProjectConfig.getAccountId())); Feature feature = new Feature(attribute.getId(), attribute.getKey(), Feature.CUSTOM_ATTRIBUTE_FEATURE_TYPE, - "value", true); + AUDIENCE_GRYFFINDOR_VALUE, true); List expectedUserFeatures = Collections.singletonList(feature); // Event Features @@ -255,17 +300,17 @@ public void createConversionEvent() throws Exception { expectedEventFeatures.add(new Feature("", "string_param", Feature.EVENT_FEATURE_TYPE, "123", false)); - assertThat(conversion.getUserFeatures(), is(expectedUserFeatures)); - assertThat(conversion.getLayerStates(), is(expectedLayerStates)); - assertThat(conversion.getEventEntityId(), is(eventType.getId())); - assertThat(conversion.getEventName(), is(eventType.getKey())); - assertThat(conversion.getEventMetrics(), is(Collections.emptyList())); + assertEquals(conversion.getUserFeatures(), expectedUserFeatures); + assertThat(conversion.getLayerStates(), containsInAnyOrder(expectedLayerStates.toArray())); + assertEquals(conversion.getEventEntityId(), eventType.getId()); + assertEquals(conversion.getEventName(), eventType.getKey()); + assertEquals(conversion.getEventMetrics(), Collections.emptyList()); assertTrue(conversion.getEventFeatures().containsAll(expectedEventFeatures)); assertTrue(expectedEventFeatures.containsAll(conversion.getEventFeatures())); assertFalse(conversion.getIsGlobalHoldback()); - assertThat(conversion.getAnonymizeIP(), is(validProjectConfig.getAnonymizeIP())); - assertThat(conversion.getClientEngine(), is(ClientEngine.JAVA_SDK.getClientEngineValue())); - assertThat(conversion.getClientVersion(), is(BuildVersionInfo.VERSION)); + assertEquals(conversion.getAnonymizeIP(), validProjectConfig.getAnonymizeIP()); + assertEquals(conversion.getClientEngine(), ClientEngine.JAVA_SDK.getClientEngineValue()); + assertEquals(conversion.getClientVersion(), BuildVersionInfo.VERSION); } /** @@ -284,27 +329,33 @@ public void createConversionParamsWithRevenue() throws Exception { // Bucket to the first variation for all experiments. for (Experiment experiment : validProjectConfig.getExperiments()) { when(mockBucketAlgorithm.bucket(experiment, userId)) - .thenReturn(experiment.getVariations().get(0)); + .thenReturn(experiment.getVariations().get(0)); } + DecisionService decisionService = new DecisionService( + mockBucketAlgorithm, + mock(ErrorHandler.class), + validProjectConfig, + mock(UserProfileService.class) + ); Map attributeMap = Collections.singletonMap(attribute.getKey(), "value"); Map eventTagMap = new HashMap(); eventTagMap.put(ReservedEventKey.REVENUE.toString(), revenue); Map experimentVariationMap = createExperimentVariationMap( validProjectConfig, - mockBucketAlgorithm, + decisionService, eventType.getKey(), userId, attributeMap); LogEvent conversionEvent = builder.createConversionEvent(validProjectConfig, experimentVariationMap, userId, - eventType.getId(), eventType.getKey(), attributeMap, - eventTagMap); + eventType.getId(), eventType.getKey(), attributeMap, + eventTagMap); Conversion conversion = gson.fromJson(conversionEvent.getBody(), Conversion.class); // we're not going to verify everything, only revenue assertThat(conversion.getEventMetrics(), - is(Collections.singletonList(new EventMetric(EventMetric.REVENUE_METRIC_TYPE, revenue)))); + is(Collections.singletonList(new EventMetric(EventMetric.REVENUE_METRIC_TYPE, revenue)))); } /** @@ -313,35 +364,52 @@ public void createConversionParamsWithRevenue() throws Exception { */ @Test public void createConversionEventForcedVariationBucketingPrecedesAudienceEval() { - EventType eventType = validProjectConfig.getEventTypes().get(0); - String userId = "testUser1"; - - Bucketer mockBucketAlgorithm = mock(Bucketer.class); - for (Experiment experiment : validProjectConfig.getExperiments()) { - when(mockBucketAlgorithm.bucket(experiment, userId)) - .thenReturn(experiment.getVariations().get(0)); + EventType eventType; + String whitelistedUserId; + if (datafileVersion == 4) { + eventType = validProjectConfig.getEventNameMapping().get(EVENT_BASIC_EVENT_KEY); + whitelistedUserId = MULTIVARIATE_EXPERIMENT_FORCED_VARIATION_USER_ID_GRED; } + else { + eventType = validProjectConfig.getEventTypes().get(0); + whitelistedUserId = "testUser1"; + } + + DecisionService decisionService = new DecisionService( + new Bucketer(validProjectConfig), + new NoOpErrorHandler(), + validProjectConfig, + mock(UserProfileService.class) + ); // attributes are empty so user won't be in the audience for experiment using the event, but bucketing // will still take place Map experimentVariationMap = createExperimentVariationMap( validProjectConfig, - mockBucketAlgorithm, + decisionService, eventType.getKey(), - userId, + whitelistedUserId, Collections.emptyMap()); LogEvent conversionEvent = builder.createConversionEvent( validProjectConfig, experimentVariationMap, - userId, + whitelistedUserId, eventType.getId(), eventType.getKey(), Collections.emptyMap(), Collections.emptyMap()); + assertNotNull(conversionEvent); Conversion conversion = gson.fromJson(conversionEvent.getBody(), Conversion.class); - // 1 experiment uses the event - assertThat(conversion.getLayerStates().size(), is(1)); + if (datafileVersion == 4) { + // 2 experiments use the event + // basic experiment has no audience + // user is whitelisted in to one audience + assertEquals(2, conversion.getLayerStates().size()); + } + else { + assertEquals(1, conversion.getLayerStates().size()); + } } /** @@ -350,32 +418,40 @@ public void createConversionEventForcedVariationBucketingPrecedesAudienceEval() */ @Test public void createConversionEventExperimentStatusPrecedesForcedVariation() { - EventType eventType = validProjectConfig.getEventTypes().get(3); - String userId = "userId"; - - Bucketer mockBucketAlgorithm = mock(Bucketer.class); - for (Experiment experiment : validProjectConfig.getExperiments()) { - when(mockBucketAlgorithm.bucket(experiment, userId)) - .thenReturn(experiment.getVariations().get(0)); + EventType eventType; + if (datafileVersion == 4) { + eventType = validProjectConfig.getEventNameMapping().get(EVENT_PAUSED_EXPERIMENT_KEY); } + else { + eventType = validProjectConfig.getEventTypes().get(3); + } + String whitelistedUserId = PAUSED_EXPERIMENT_FORCED_VARIATION_USER_ID_CONTROL; + + Bucketer bucketer = spy(new Bucketer(validProjectConfig)); + DecisionService decisionService = new DecisionService( + bucketer, + mock(ErrorHandler.class), + validProjectConfig, + mock(UserProfileService.class) + ); Map experimentVariationMap = createExperimentVariationMap( validProjectConfig, - mockBucketAlgorithm, + decisionService, eventType.getKey(), - userId, + whitelistedUserId, Collections.emptyMap()); LogEvent conversionEvent = builder.createConversionEvent( validProjectConfig, experimentVariationMap, - userId, + whitelistedUserId, eventType.getId(), eventType.getKey(), Collections.emptyMap(), Collections.emptyMap()); for (Experiment experiment : validProjectConfig.getExperiments()) { - verify(mockBucketAlgorithm, never()).bucket(experiment, userId); + verify(bucketer, never()).bucket(experiment, whitelistedUserId); } assertNull(conversionEvent); @@ -396,11 +472,17 @@ public void createConversionEventAndroidClientEngineClientVersion() throws Excep when(mockBucketAlgorithm.bucket(experiment, userId)) .thenReturn(experiment.getVariations().get(0)); } + DecisionService decisionService = new DecisionService( + mockBucketAlgorithm, + mock(ErrorHandler.class), + validProjectConfig, + mock(UserProfileService.class) + ); Map attributeMap = Collections.singletonMap(attribute.getKey(), "value"); Map experimentVariationMap = createExperimentVariationMap( validProjectConfig, - mockBucketAlgorithm, + decisionService, eventType.getKey(), userId, attributeMap); @@ -484,7 +566,7 @@ public void createConversionEventReturnsNullWhenExperimentVariationMapIsEmpty() //========== helper methods =========// public static Map createExperimentVariationMap(ProjectConfig projectConfig, - Bucketer bucketer, + DecisionService decisionService, String eventName, String userId, @Nullable Map attributes) { @@ -492,9 +574,8 @@ public static Map createExperimentVariationMap(ProjectCon List eventExperiments = projectConfig.getExperimentsForEventKey(eventName); Map experimentVariationMap = new HashMap(eventExperiments.size()); for (Experiment experiment : eventExperiments) { - if (ExperimentUtils.isExperimentActive(experiment) - && experiment.isRunning()) { - Variation variation = bucketer.bucket(experiment, userId); + if (experiment.isRunning()) { + Variation variation = decisionService.getVariation(experiment, userId, attributes); if (variation != null) { experimentVariationMap.put(experiment, variation); }