From 02be21e08fdf5d77f89ec5d0111d44159cb25bc0 Mon Sep 17 00:00:00 2001 From: Gaston Thea Date: Fri, 13 Dec 2024 11:12:49 -0300 Subject: [PATCH 1/3] WIP tests --- .../assets/split_changes_imp_toggle.json | 124 ++++++++++++ .../java/helper/IntegrationHelper.java | 4 + .../toggle/ImpressionsToggleTest.java | 190 ++++++++++++++++++ 3 files changed, 318 insertions(+) create mode 100644 src/androidTest/assets/split_changes_imp_toggle.json create mode 100644 src/androidTest/java/tests/integration/toggle/ImpressionsToggleTest.java diff --git a/src/androidTest/assets/split_changes_imp_toggle.json b/src/androidTest/assets/split_changes_imp_toggle.json new file mode 100644 index 000000000..d1f7196ce --- /dev/null +++ b/src/androidTest/assets/split_changes_imp_toggle.json @@ -0,0 +1,124 @@ +{ + "splits": [ + { + "trafficTypeName": "user", + "name": "tracked", + "trafficAllocation": 100, + "trafficAllocationSeed": -285565213, + "seed": -1992295819, + "status": "ACTIVE", + "killed": false, + "defaultTreatment": "off", + "changeNumber": 1506703262916, + "algo": 2, + "trackImpressions": true, + "conditions": [ + { + "conditionType": "ROLLOUT", + "matcherGroup": { + "combiner": "AND", + "matchers": [ + { + "keySelector": { + "trafficType": "client", + "attribute": null + }, + "matcherType": "IN_SEGMENT", + "negate": false, + "userDefinedSegmentMatcherData": { + "segmentName": "new_segment" + }, + "whitelistMatcherData": null, + "unaryNumericMatcherData": null, + "betweenMatcherData": null, + "booleanMatcherData": null, + "dependencyMatcherData": null, + "stringMatcherData": null + } + ] + }, + "partitions": [ + { + "treatment": "on", + "size": 0 + }, + { + "treatment": "off", + "size": 0 + }, + { + "treatment": "free", + "size": 100 + }, + { + "treatment": "conta", + "size": 0 + } + ], + "label": "in segment new_segment" + } + ] + }, + { + "trafficTypeName": "user", + "name": "not_tracked", + "trafficAllocation": 100, + "trafficAllocationSeed": -285565213, + "seed": -1992295819, + "status": "ACTIVE", + "killed": false, + "defaultTreatment": "off", + "changeNumber": 1506703262916, + "algo": 2, + "trackImpressions": false, + "conditions": [ + { + "conditionType": "ROLLOUT", + "matcherGroup": { + "combiner": "AND", + "matchers": [ + { + "keySelector": { + "trafficType": "client", + "attribute": null + }, + "matcherType": "IN_SEGMENT", + "negate": false, + "userDefinedSegmentMatcherData": { + "segmentName": "new_segment" + }, + "whitelistMatcherData": null, + "unaryNumericMatcherData": null, + "betweenMatcherData": null, + "booleanMatcherData": null, + "dependencyMatcherData": null, + "stringMatcherData": null + } + ] + }, + "partitions": [ + { + "treatment": "on", + "size": 0 + }, + { + "treatment": "off", + "size": 0 + }, + { + "treatment": "free", + "size": 100 + }, + { + "treatment": "conta", + "size": 0 + } + ], + "label": "in segment new_segment" + } + ] + } + ], + "since": 1506703262916, + "till": 1506703262916 +} diff --git a/src/androidTest/java/helper/IntegrationHelper.java b/src/androidTest/java/helper/IntegrationHelper.java index 528f730fa..6144c6a7b 100644 --- a/src/androidTest/java/helper/IntegrationHelper.java +++ b/src/androidTest/java/helper/IntegrationHelper.java @@ -442,5 +442,9 @@ public interface StreamingResponseClosure { public static class ServicePath { public static final String MEMBERSHIPS = "memberships"; public static final String SPLIT_CHANGES = "splitChanges"; + public static final String EVENTS = "events"; + public static final String UNIQUE_KEYS = "keys/cs"; + public static final String COUNT = "testImpressions/count"; + public static final String IMPRESSIONS = "testImpressions/bulk"; } } diff --git a/src/androidTest/java/tests/integration/toggle/ImpressionsToggleTest.java b/src/androidTest/java/tests/integration/toggle/ImpressionsToggleTest.java new file mode 100644 index 000000000..9c2a7bb69 --- /dev/null +++ b/src/androidTest/java/tests/integration/toggle/ImpressionsToggleTest.java @@ -0,0 +1,190 @@ +package tests.integration.toggle; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertTrue; +import static org.junit.Assert.fail; +import static helper.IntegrationHelper.buildFactory; +import static helper.IntegrationHelper.emptyAllSegments; +import static helper.IntegrationHelper.getSinceFromUri; + +import android.content.Context; + +import androidx.test.platform.app.InstrumentationRegistry; + +import org.junit.Before; +import org.junit.Test; + +import java.util.List; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicReference; + +import helper.DatabaseHelper; +import helper.FileHelper; +import helper.IntegrationHelper; +import io.split.android.client.ServiceEndpoints; +import io.split.android.client.SplitClient; +import io.split.android.client.SplitClientConfig; +import io.split.android.client.SplitFactory; +import io.split.android.client.SplitManager; +import io.split.android.client.api.SplitView; +import io.split.android.client.dtos.SplitChange; +import io.split.android.client.events.SplitEvent; +import io.split.android.client.service.impressions.ImpressionsMode; +import io.split.android.client.utils.Json; +import io.split.android.client.utils.logger.SplitLogLevel; +import okhttp3.mockwebserver.Dispatcher; +import okhttp3.mockwebserver.MockResponse; +import okhttp3.mockwebserver.MockWebServer; +import okhttp3.mockwebserver.RecordedRequest; +import tests.integration.shared.TestingHelper; + +public class ImpressionsToggleTest { + + private final Context mContext = InstrumentationRegistry.getInstrumentation().getContext(); + private CountDownLatch mRequestCountdownLatch; + private MockWebServer mWebServer; + + private final AtomicReference mCountBody = new AtomicReference<>(null); + private final AtomicReference mImpressionsBody = new AtomicReference<>(null); + private final AtomicReference mUniqueKeysBody = new AtomicReference<>(null); + + @Before + public void setUp() { + setupServer(); + mRequestCountdownLatch = new CountDownLatch(1); + mCountBody.set(null); + mImpressionsBody.set(null); + mUniqueKeysBody.set(null); + } + + @Test + public void managerContainsProperty() throws InterruptedException { + SplitFactory splitFactory = getReadyFactory(ImpressionsMode.OPTIMIZED); + + SplitManager manager = splitFactory.manager(); + List splits = manager.splits(); + + assertTrue(manager.split("tracked").trackImpressions); + assertFalse(manager.split("not_tracked").trackImpressions); + assertEquals(2, splits.size()); + } + + @Test + public void test() throws InterruptedException { + // 1. Initialize SDK in impressions NONE mode + SplitFactory splitFactory = getReadyFactory(ImpressionsMode.NONE); + + // 2. Fetch splitChanges with both flags with trackImpressions true & false + SplitClient client = splitFactory.client(); + client.getTreatment("tracked"); + client.getTreatment("not_tracked"); + + // 3. Verify all counts & mtks are tracked + client.flush(); + + Thread.sleep(2000); + assertNotNull(mCountBody.get()); + assertNotNull(mUniqueKeysBody.get()); + assertNull(mImpressionsBody.get()); + } + + @Test + public void test2() { + // 1. Initialize SDK in impressions DEBUG mode + // 2. Fetch splitChanges with both flags with trackImpressions true & false + // 3. Verify counts & MTKs are tracked only for trackImpressions false + // 4. Verify impressions are tracked for trackImpressions true + } + + @Test + public void test3() { + // 1. Initialize SDK in impressions OPTIMIZED mode + // 2. Fetch splitChanges with both flags with trackImpressions true & false + // 3. Verify counts & MTKs are tracked only for trackImpressions false + // 4. Verify impressions are tracked for trackImpressions true + } + + private SplitFactory getReadyFactory(ImpressionsMode impressionsMode) throws InterruptedException { + SplitFactory splitFactory = getSplitFactory(impressionsMode); + CountDownLatch latch = new CountDownLatch(1); + splitFactory.client().on(SplitEvent.SDK_READY, new TestingHelper.TestEventTask(latch)); + mRequestCountdownLatch.countDown(); + + boolean await = latch.await(5, TimeUnit.SECONDS); + + if (!await) { + fail("Client was not ready"); + } + + return splitFactory; + } + + private SplitFactory getSplitFactory(ImpressionsMode impressionsMode) { + final String url = mWebServer.url("/").url().toString(); + ServiceEndpoints endpoints = ServiceEndpoints.builder() + .apiEndpoint(url) + .eventsEndpoint(url) + .telemetryServiceEndpoint(url) + .build(); + SplitClientConfig config = new SplitClientConfig.Builder() + .serviceEndpoints(endpoints) + .impressionsMode(impressionsMode) + .streamingEnabled(false) + .featuresRefreshRate(9999) + .segmentsRefreshRate(9999) + .impressionsRefreshRate(9999) + .logLevel(SplitLogLevel.VERBOSE) + .streamingEnabled(false) + .build(); + + return buildFactory(IntegrationHelper.dummyApiKey(), IntegrationHelper.dummyUserKey(), config, + mContext, null, DatabaseHelper.getTestDatabase(mContext)); + } + + private void setupServer() { + mWebServer = new MockWebServer(); + + final Dispatcher dispatcher = new Dispatcher() { + + @Override + public MockResponse dispatch(RecordedRequest request) throws InterruptedException { + mRequestCountdownLatch.await(); + if (request.getPath().contains("/" + IntegrationHelper.ServicePath.MEMBERSHIPS)) { + return new MockResponse().setResponseCode(200).setBody(emptyAllSegments()); + } else if (request.getPath().contains("/" + IntegrationHelper.ServicePath.SPLIT_CHANGES)) { + String sinceFromUri = getSinceFromUri(request.getRequestUrl().uri()); + if (sinceFromUri.equals("-1")) { + return new MockResponse().setResponseCode(200).setBody(loadSplitChanges()); + } else { + return new MockResponse().setResponseCode(200) + .setBody(IntegrationHelper.emptySplitChanges(1506703262916L, 1506703262916L)); + } + } else if (request.getPath().contains("/" + IntegrationHelper.ServicePath.COUNT)) { + mCountBody.set(request.getBody().readUtf8()); + return new MockResponse().setResponseCode(200); + } else if (request.getPath().contains("/" + IntegrationHelper.ServicePath.IMPRESSIONS)) { + mImpressionsBody.set(request.getBody().readUtf8()); + return new MockResponse().setResponseCode(200); + } else if (request.getPath().contains("/" + IntegrationHelper.ServicePath.UNIQUE_KEYS)) { + mUniqueKeysBody.set(request.getBody().readUtf8()); + return new MockResponse().setResponseCode(200); + } else { + return new MockResponse().setResponseCode(404); + } + } + }; + mWebServer.setDispatcher(dispatcher); + } + + private String loadSplitChanges() { + FileHelper fileHelper = new FileHelper(); + String change = fileHelper.loadFileContent(mContext, "split_changes_imp_toggle.json"); + SplitChange parsedChange = Json.fromJson(change, SplitChange.class); + parsedChange.since = parsedChange.till; + return Json.toJson(parsedChange); + } +} From a79d535624fcd55ae83e8650f9fc8b8ffe12716b Mon Sep 17 00:00:00 2001 From: Gaston Thea Date: Fri, 13 Dec 2024 11:42:48 -0300 Subject: [PATCH 2/3] WIP test --- .../java/helper/DatabaseHelper.java | 5 +++- .../toggle/ImpressionsToggleTest.java | 24 +++++++++++++++---- 2 files changed, 24 insertions(+), 5 deletions(-) diff --git a/src/androidTest/java/helper/DatabaseHelper.java b/src/androidTest/java/helper/DatabaseHelper.java index e71be3479..57646fcd2 100644 --- a/src/androidTest/java/helper/DatabaseHelper.java +++ b/src/androidTest/java/helper/DatabaseHelper.java @@ -17,9 +17,12 @@ public static boolean removeDatabaseFile(String name) { } public static SplitRoomDatabase getTestDatabase(Context context) { - return Room.inMemoryDatabaseBuilder(context, SplitRoomDatabase.class) + SplitRoomDatabase database = Room.inMemoryDatabaseBuilder(context, SplitRoomDatabase.class) .fallbackToDestructiveMigration() .allowMainThreadQueries() .build(); + + database.clearAllTables(); + return database; } } diff --git a/src/androidTest/java/tests/integration/toggle/ImpressionsToggleTest.java b/src/androidTest/java/tests/integration/toggle/ImpressionsToggleTest.java index 9c2a7bb69..f08e65c76 100644 --- a/src/androidTest/java/tests/integration/toggle/ImpressionsToggleTest.java +++ b/src/androidTest/java/tests/integration/toggle/ImpressionsToggleTest.java @@ -35,6 +35,7 @@ import io.split.android.client.events.SplitEvent; import io.split.android.client.service.impressions.ImpressionsMode; import io.split.android.client.utils.Json; +import io.split.android.client.utils.logger.Logger; import io.split.android.client.utils.logger.SplitLogLevel; import okhttp3.mockwebserver.Dispatcher; import okhttp3.mockwebserver.MockResponse; @@ -52,6 +53,10 @@ public class ImpressionsToggleTest { private final AtomicReference mImpressionsBody = new AtomicReference<>(null); private final AtomicReference mUniqueKeysBody = new AtomicReference<>(null); + private CountDownLatch mCountLatch; + private CountDownLatch mImpressionsLatch; + private CountDownLatch mUniqueKeysLatch; + @Before public void setUp() { setupServer(); @@ -59,6 +64,10 @@ public void setUp() { mCountBody.set(null); mImpressionsBody.set(null); mUniqueKeysBody.set(null); + + mCountLatch = new CountDownLatch(1); + mImpressionsLatch = new CountDownLatch(1); + mUniqueKeysLatch = new CountDownLatch(1); } @Test @@ -74,19 +83,22 @@ public void managerContainsProperty() throws InterruptedException { } @Test - public void test() throws InterruptedException { + public void testNoneMode() throws InterruptedException { // 1. Initialize SDK in impressions NONE mode SplitFactory splitFactory = getReadyFactory(ImpressionsMode.NONE); // 2. Fetch splitChanges with both flags with trackImpressions true & false SplitClient client = splitFactory.client(); - client.getTreatment("tracked"); - client.getTreatment("not_tracked"); + String trackedTreatment = client.getTreatment("tracked"); + String notTrackedTreatment = client.getTreatment("not_tracked"); + Thread.sleep(200); // 3. Verify all counts & mtks are tracked client.flush(); - Thread.sleep(2000); + mUniqueKeysLatch.await(5, TimeUnit.SECONDS); + mCountLatch.await(5, TimeUnit.SECONDS); + assertNotNull(mCountBody.get()); assertNotNull(mUniqueKeysBody.get()); assertNull(mImpressionsBody.get()); @@ -153,6 +165,7 @@ private void setupServer() { @Override public MockResponse dispatch(RecordedRequest request) throws InterruptedException { mRequestCountdownLatch.await(); + Logger.e("Path is: " + request.getPath()); if (request.getPath().contains("/" + IntegrationHelper.ServicePath.MEMBERSHIPS)) { return new MockResponse().setResponseCode(200).setBody(emptyAllSegments()); } else if (request.getPath().contains("/" + IntegrationHelper.ServicePath.SPLIT_CHANGES)) { @@ -164,12 +177,15 @@ public MockResponse dispatch(RecordedRequest request) throws InterruptedExceptio .setBody(IntegrationHelper.emptySplitChanges(1506703262916L, 1506703262916L)); } } else if (request.getPath().contains("/" + IntegrationHelper.ServicePath.COUNT)) { + mCountLatch.countDown(); mCountBody.set(request.getBody().readUtf8()); return new MockResponse().setResponseCode(200); } else if (request.getPath().contains("/" + IntegrationHelper.ServicePath.IMPRESSIONS)) { + mImpressionsLatch.countDown(); mImpressionsBody.set(request.getBody().readUtf8()); return new MockResponse().setResponseCode(200); } else if (request.getPath().contains("/" + IntegrationHelper.ServicePath.UNIQUE_KEYS)) { + mUniqueKeysLatch.countDown(); mUniqueKeysBody.set(request.getBody().readUtf8()); return new MockResponse().setResponseCode(200); } else { From f547b3afa09caf78110b10d0a15cbbf0ceb655da Mon Sep 17 00:00:00 2001 From: Gaston Thea Date: Fri, 13 Dec 2024 15:42:04 -0300 Subject: [PATCH 3/3] Better tests --- .../toggle/ImpressionsToggleTest.java | 111 +++++++++++++++--- 1 file changed, 96 insertions(+), 15 deletions(-) diff --git a/src/androidTest/java/tests/integration/toggle/ImpressionsToggleTest.java b/src/androidTest/java/tests/integration/toggle/ImpressionsToggleTest.java index f08e65c76..8d3f4c352 100644 --- a/src/androidTest/java/tests/integration/toggle/ImpressionsToggleTest.java +++ b/src/androidTest/java/tests/integration/toggle/ImpressionsToggleTest.java @@ -14,9 +14,13 @@ import androidx.test.platform.app.InstrumentationRegistry; +import com.google.common.reflect.TypeToken; + import org.junit.Before; import org.junit.Test; +import java.lang.reflect.Type; +import java.util.Arrays; import java.util.List; import java.util.concurrent.CountDownLatch; import java.util.concurrent.TimeUnit; @@ -32,8 +36,11 @@ import io.split.android.client.SplitManager; import io.split.android.client.api.SplitView; import io.split.android.client.dtos.SplitChange; +import io.split.android.client.dtos.TestImpressions; import io.split.android.client.events.SplitEvent; +import io.split.android.client.service.impressions.ImpressionsCount; import io.split.android.client.service.impressions.ImpressionsMode; +import io.split.android.client.service.impressions.unique.MTK; import io.split.android.client.utils.Json; import io.split.android.client.utils.logger.Logger; import io.split.android.client.utils.logger.SplitLogLevel; @@ -89,35 +96,109 @@ public void testNoneMode() throws InterruptedException { // 2. Fetch splitChanges with both flags with trackImpressions true & false SplitClient client = splitFactory.client(); - String trackedTreatment = client.getTreatment("tracked"); - String notTrackedTreatment = client.getTreatment("not_tracked"); + client.getTreatment("tracked"); + client.getTreatment("not_tracked"); Thread.sleep(200); - // 3. Verify all counts & mtks are tracked + // 3. Flush client.flush(); + // 4. Wait for requests to be sent mUniqueKeysLatch.await(5, TimeUnit.SECONDS); mCountLatch.await(5, TimeUnit.SECONDS); + boolean impressionsAwait = mImpressionsLatch.await(3, TimeUnit.SECONDS); - assertNotNull(mCountBody.get()); - assertNotNull(mUniqueKeysBody.get()); - assertNull(mImpressionsBody.get()); + // 5. Verify request bodies + verifyOnlyNone(impressionsAwait); } @Test - public void test2() { + public void testDebugMode() throws InterruptedException { // 1. Initialize SDK in impressions DEBUG mode + SplitFactory splitFactory = getReadyFactory(ImpressionsMode.DEBUG); + // 2. Fetch splitChanges with both flags with trackImpressions true & false - // 3. Verify counts & MTKs are tracked only for trackImpressions false - // 4. Verify impressions are tracked for trackImpressions true + SplitClient client = splitFactory.client(); + client.getTreatment("tracked"); + client.getTreatment("not_tracked"); + Thread.sleep(500); + + // 3. Flush + client.flush(); + + // 4. Wait for requests to be sent + mUniqueKeysLatch.await(5, TimeUnit.SECONDS); + mCountLatch.await(5, TimeUnit.SECONDS); + mImpressionsLatch.await(5, TimeUnit.SECONDS); + + // 5. Verify request bodies + verify(); } @Test - public void test3() { + public void testOptimizedMode() throws InterruptedException { // 1. Initialize SDK in impressions OPTIMIZED mode + SplitFactory splitFactory = getReadyFactory(ImpressionsMode.OPTIMIZED); + // 2. Fetch splitChanges with both flags with trackImpressions true & false - // 3. Verify counts & MTKs are tracked only for trackImpressions false - // 4. Verify impressions are tracked for trackImpressions true + SplitClient client = splitFactory.client(); + client.getTreatment("tracked"); + client.getTreatment("not_tracked"); + Thread.sleep(200); + + // 3. Flush + client.flush(); + + // 4. Wait for requests to be sent + mUniqueKeysLatch.await(5, TimeUnit.SECONDS); + mCountLatch.await(5, TimeUnit.SECONDS); + mImpressionsLatch.await(5, TimeUnit.SECONDS); + + // 5. Verify request bodies + verify(); + } + + private void verifyOnlyNone(boolean impressionsAwait) { + // a. Counts + String countBody = mCountBody.get(); + ImpressionsCount impressionsCount = Json.fromJson(countBody, ImpressionsCount.class); + assertEquals(2, impressionsCount.perFeature.size()); + assertTrue(impressionsCount.perFeature.stream().anyMatch(c -> c.feature.equals("tracked") && c.count == 1)); + assertTrue(impressionsCount.perFeature.stream().anyMatch(c -> c.feature.equals("not_tracked") && c.count == 1)); + + // b. MTKs + String uniqueBody = mUniqueKeysBody.get(); + MTK mtk = Json.fromJson(uniqueBody, MTK.class); + assertEquals(1, mtk.getKeys().size()); + assertTrue(mtk.getKeys().get(0).getFeatures().containsAll(Arrays.asList("not_tracked", "tracked"))); + assertEquals("CUSTOMER_ID", mtk.getKeys().get(0).getKey()); + + // c. Impressions (no impressions) + assertNull(mImpressionsBody.get()); + assertFalse(impressionsAwait); + } + + private void verify() { + // a. Counts + String countBody = mCountBody.get(); + ImpressionsCount impressionsCount = Json.fromJson(countBody, ImpressionsCount.class); + assertEquals(1, impressionsCount.perFeature.size()); + assertEquals("not_tracked", impressionsCount.perFeature.get(0).feature); + + // b. MTKs + String uniqueBody = mUniqueKeysBody.get(); + MTK mtk = Json.fromJson(uniqueBody, MTK.class); + assertEquals(1, mtk.getKeys().size()); + assertTrue(mtk.getKeys().get(0).getFeatures().containsAll(Arrays.asList("not_tracked"))); + assertEquals(1, mtk.getKeys().get(0).getFeatures().size()); + assertNotNull(uniqueBody); + + // c. Impressions + String impressionsBody = mImpressionsBody.get(); + Type listTypeToken = new TypeToken>(){}.getType(); + List impressions = Json.fromJson(impressionsBody, listTypeToken); + assertEquals(1, impressions.size()); + assertEquals("tracked", impressions.get(0).testName); } private SplitFactory getReadyFactory(ImpressionsMode impressionsMode) throws InterruptedException { @@ -177,16 +258,16 @@ public MockResponse dispatch(RecordedRequest request) throws InterruptedExceptio .setBody(IntegrationHelper.emptySplitChanges(1506703262916L, 1506703262916L)); } } else if (request.getPath().contains("/" + IntegrationHelper.ServicePath.COUNT)) { - mCountLatch.countDown(); mCountBody.set(request.getBody().readUtf8()); + mCountLatch.countDown(); return new MockResponse().setResponseCode(200); } else if (request.getPath().contains("/" + IntegrationHelper.ServicePath.IMPRESSIONS)) { - mImpressionsLatch.countDown(); mImpressionsBody.set(request.getBody().readUtf8()); + mImpressionsLatch.countDown(); return new MockResponse().setResponseCode(200); } else if (request.getPath().contains("/" + IntegrationHelper.ServicePath.UNIQUE_KEYS)) { - mUniqueKeysLatch.countDown(); mUniqueKeysBody.set(request.getBody().readUtf8()); + mUniqueKeysLatch.countDown(); return new MockResponse().setResponseCode(200); } else { return new MockResponse().setResponseCode(404);