Skip to content

Commit e0b54bd

Browse files
authored
Merge pull request #33 from optimizely/josh/test-app-enhancements
Josh/test app enhancements
2 parents 471405a + cfd85c6 commit e0b54bd

File tree

27 files changed

+744
-130
lines changed

27 files changed

+744
-130
lines changed

android-sdk/src/main/java/com/optimizely/ab/android/sdk/AndroidOptimizely.java

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -113,6 +113,7 @@ public void track(@NonNull String eventName,
113113
@NonNull Map<String, String> attributes) throws UnknownEventTypeException {
114114
if (optimizely != null) {
115115
optimizely.track(eventName, userId, attributes);
116+
116117
} else {
117118
logger.warn("Optimizely is not initialized, could not track event {} for user {}" +
118119
" with attributes", eventName, userId);

android-sdk/src/main/java/com/optimizely/ab/android/sdk/DataFileService.java

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,10 @@
3535
* @hide
3636
*/
3737
public class DataFileService extends Service {
38-
static final String EXTRA_PROJECT_ID = "com.optimizely.ab.android.EXTRA_PROJECT_ID";
38+
/**
39+
* Extra containing the project id this instance of Optimizely was built with
40+
*/
41+
public static final String EXTRA_PROJECT_ID = "com.optimizely.ab.android.EXTRA_PROJECT_ID";
3942
@NonNull private final IBinder binder = new LocalBinder();
4043
Logger logger = LoggerFactory.getLogger(getClass());
4144
private boolean isBound;

android-sdk/src/main/java/com/optimizely/ab/android/sdk/OptimizelyManager.java

Lines changed: 18 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@
3333
import android.support.annotation.Nullable;
3434
import android.support.annotation.RawRes;
3535
import android.support.annotation.RequiresApi;
36+
import android.support.annotation.VisibleForTesting;
3637

3738
import com.optimizely.ab.Optimizely;
3839
import com.optimizely.ab.android.event_handler.OptlyEventHandler;
@@ -66,6 +67,7 @@ public class OptimizelyManager {
6667
@NonNull private final Logger logger;
6768
@Nullable private DataFileServiceConnection dataFileServiceConnection;
6869
@Nullable private OptimizelyStartListener optimizelyStartListener;
70+
@Nullable private UserExperimentRecord userExperimentRecord;
6971

7072
OptimizelyManager(@NonNull String projectId,
7173
@NonNull Long eventHandlerDispatchInterval,
@@ -265,6 +267,7 @@ protected void onPostExecute(UserExperimentRecord userExperimentRecord) {
265267

266268
try {
267269
OptimizelyManager.androidOptimizely = buildOptimizely(context, dataFile, userExperimentRecord);
270+
OptimizelyManager.this.userExperimentRecord = userExperimentRecord;
268271
logger.info("Sending Optimizely instance to listener");
269272

270273
if (optimizelyStartListener != null) {
@@ -295,6 +298,21 @@ private AndroidOptimizely buildOptimizely(@NonNull Context context, @NonNull Str
295298
return new AndroidOptimizely(optimizely, LoggerFactory.getLogger(AndroidOptimizely.class));
296299
}
297300

301+
@VisibleForTesting
302+
public UserExperimentRecord getUserExperimentRecord() {
303+
return userExperimentRecord;
304+
}
305+
306+
private boolean isAndroidVersionSupported() {
307+
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.ICE_CREAM_SANDWICH) {
308+
return true;
309+
} else {
310+
logger.warn("Optimizely will not work on this phone. It's Android version {} is less the minimum supported" +
311+
"version {}", Build.VERSION.SDK_INT, Build.VERSION_CODES.ICE_CREAM_SANDWICH);
312+
return false;
313+
}
314+
}
315+
298316
@RequiresApi(api = Build.VERSION_CODES.ICE_CREAM_SANDWICH)
299317
static class OptlyActivityLifecycleCallbacks implements Application.ActivityLifecycleCallbacks {
300318

@@ -486,14 +504,4 @@ public OptimizelyManager build() {
486504

487505
}
488506
}
489-
490-
private boolean isAndroidVersionSupported() {
491-
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.ICE_CREAM_SANDWICH) {
492-
return true;
493-
} else {
494-
logger.warn("Optimizely will not work on this phone. It's Android version {} is less the minimum supported" +
495-
"version {}", Build.VERSION.SDK_INT, Build.VERSION_CODES.ICE_CREAM_SANDWICH);
496-
return false;
497-
}
498-
}
499507
}

event-handler/src/main/java/com/optimizely/ab/android/event_handler/EventDispatcher.java

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@
2222
import android.support.annotation.NonNull;
2323
import android.util.Pair;
2424

25+
import com.optimizely.ab.android.shared.CountingIdlingResourceManager;
2526
import com.optimizely.ab.android.shared.OptlyStorage;
2627
import com.optimizely.ab.android.shared.ServiceScheduler;
2728

@@ -136,6 +137,8 @@ private boolean dispatch(Event event) {
136137
boolean eventWasSent = eventClient.sendEvent(event);
137138

138139
if (eventWasSent) {
140+
CountingIdlingResourceManager.decrement();
141+
CountingIdlingResourceManager.recordEvent(new Pair<>(event.getURL().toString(), event.getRequestBody()));
139142
return true;
140143
} else {
141144
boolean eventWasStored = eventDAO.storeEvent(event);
@@ -148,6 +151,4 @@ private boolean dispatch(Event event) {
148151
}
149152
}
150153
}
151-
152-
153154
}

shared/build.gradle

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,7 @@ dependencies {
5454
compile "com.noveogroup.android:android-logger:$android_logger_ver"
5555
compile "com.optimizely.ab:core-api:$java_core_ver"
5656
compile "com.google.code.gson:gson:$gson_ver"
57+
compile "com.android.support.test.espresso:espresso-idling-resource:$espresso_ver"
5758
provided "com.android.support:support-annotations:$support_annotations_ver"
5859

5960
testCompile "junit:junit:$junit_ver"
Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,81 @@
1+
/*
2+
* Copyright 2016, Optimizely
3+
* <p/>
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
* <p/>
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
* <p/>
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package com.optimizely.ab.android.shared;
18+
19+
import android.support.annotation.NonNull;
20+
import android.support.annotation.Nullable;
21+
import android.support.annotation.VisibleForTesting;
22+
import android.support.test.espresso.idling.CountingIdlingResource;
23+
import android.util.Pair;
24+
25+
import java.util.LinkedList;
26+
import java.util.List;
27+
28+
/**
29+
* Manages an Espresso {@link CountingIdlingResource}
30+
*/
31+
@VisibleForTesting
32+
public class CountingIdlingResourceManager {
33+
34+
@Nullable private static CountingIdlingResource countingIdlingResource;
35+
@NonNull private static List<Pair<String, String>> eventList = new LinkedList<>();
36+
37+
@VisibleForTesting
38+
@Nullable
39+
public static CountingIdlingResource getIdlingResource() {
40+
if (countingIdlingResource == null) {
41+
countingIdlingResource = new CountingIdlingResource("optimizely", true);
42+
}
43+
return countingIdlingResource;
44+
}
45+
46+
@VisibleForTesting
47+
public static void setIdlingResource(@NonNull CountingIdlingResource countingIdlingResource) {
48+
CountingIdlingResourceManager.countingIdlingResource = countingIdlingResource;
49+
}
50+
51+
@VisibleForTesting
52+
public static void increment() {
53+
if (countingIdlingResource != null) {
54+
countingIdlingResource.increment();
55+
}
56+
}
57+
58+
@VisibleForTesting
59+
public static void decrement() {
60+
if (countingIdlingResource != null) {
61+
countingIdlingResource.decrement();
62+
}
63+
}
64+
65+
@VisibleForTesting
66+
public static void recordEvent(Pair<String, String> event) {
67+
if (countingIdlingResource != null) {
68+
eventList.add(event);
69+
}
70+
}
71+
72+
@VisibleForTesting
73+
public static void clearEvents() {
74+
eventList.clear();
75+
}
76+
77+
@VisibleForTesting
78+
public static List<Pair<String, String>> getEvents() {
79+
return eventList;
80+
}
81+
}

shared/src/main/java/com/optimizely/ab/android/shared/ServiceScheduler.java

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -85,6 +85,16 @@ public void unschedule(Intent intent) {
8585
logger.info("Unscheduled {}", intent.getComponent().toShortString());
8686
}
8787

88+
/**
89+
* Is an {@link Intent} for a service scheduled
90+
* @param intent the intent in question
91+
* @return is it scheduled or not
92+
* @hide
93+
*/
94+
public boolean isScheduled(Intent intent) {
95+
return pendingIntentFactory.hasPendingIntent(intent);
96+
}
97+
8898
/**
8999
* Handles the complexities around PendingIntent flags
90100
*

test-app/build.gradle

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ android {
55
buildToolsVersion build_tools_version
66

77
defaultConfig {
8-
minSdkVersion min_sdk_version
8+
minSdkVersion 19
99
targetSdkVersion target_sdk_version
1010
versionCode 1
1111
versionName "1.0"
Lines changed: 173 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,173 @@
1+
/*
2+
* Copyright 2016, Optimizely
3+
* <p/>
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
* <p/>
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
* <p/>
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
package com.optimizely.ab.android.test_app;
17+
18+
import android.app.AlarmManager;
19+
import android.content.Context;
20+
import android.content.Intent;
21+
import android.support.test.InstrumentationRegistry;
22+
import android.support.test.espresso.Espresso;
23+
import android.support.test.espresso.idling.CountingIdlingResource;
24+
import android.support.test.filters.LargeTest;
25+
import android.support.test.rule.ActivityTestRule;
26+
import android.support.test.runner.AndroidJUnit4;
27+
import android.util.Pair;
28+
29+
import com.optimizely.ab.android.event_handler.EventIntentService;
30+
import com.optimizely.ab.android.sdk.DataFileService;
31+
import com.optimizely.ab.android.shared.CountingIdlingResourceManager;
32+
import com.optimizely.ab.android.shared.ServiceScheduler;
33+
import com.optimizely.ab.bucketing.UserExperimentRecord;
34+
35+
import org.junit.Rule;
36+
import org.junit.Test;
37+
import org.junit.rules.ExternalResource;
38+
import org.junit.rules.RuleChain;
39+
import org.junit.rules.TestRule;
40+
import org.junit.runner.RunWith;
41+
import org.slf4j.LoggerFactory;
42+
43+
import java.util.Iterator;
44+
import java.util.List;
45+
46+
import static android.support.test.espresso.Espresso.onView;
47+
import static android.support.test.espresso.action.ViewActions.click;
48+
import static android.support.test.espresso.assertion.ViewAssertions.matches;
49+
import static android.support.test.espresso.matcher.ViewMatchers.withId;
50+
import static android.support.test.espresso.matcher.ViewMatchers.withText;
51+
import static junit.framework.Assert.assertFalse;
52+
import static junit.framework.Assert.assertNull;
53+
import static junit.framework.Assert.assertTrue;
54+
55+
@RunWith(AndroidJUnit4.class)
56+
@LargeTest
57+
public class MainActivityEspressoTest {
58+
59+
private Context context = InstrumentationRegistry.getTargetContext();
60+
private CountingIdlingResource countingIdlingResource;
61+
private ServiceScheduler serviceScheduler;
62+
private Intent dataFileServiceIntent, eventIntentService;
63+
64+
private ActivityTestRule<MainActivity> activityTestRule = new ActivityTestRule<>(MainActivity.class);
65+
@Rule public TestRule chain = RuleChain
66+
.outerRule(new ExternalResource() {
67+
@Override
68+
protected void before() throws Throwable {
69+
super.before();
70+
countingIdlingResource = CountingIdlingResourceManager.getIdlingResource();
71+
// To prove that the test fails, omit this call:
72+
Espresso.registerIdlingResources(countingIdlingResource);
73+
}
74+
75+
@Override
76+
protected void after() {
77+
super.after();
78+
Espresso.unregisterIdlingResources(countingIdlingResource);
79+
}
80+
})
81+
.around(new ExternalResource() {
82+
@Override
83+
protected void before() throws Throwable {
84+
super.before();
85+
// Set the user's id to the test user that is in the whitelist.
86+
context.getSharedPreferences("user", Context.MODE_PRIVATE).edit()
87+
.putString("userId", "test_user")
88+
.apply();
89+
}
90+
91+
@Override
92+
protected void after() {
93+
super.after();
94+
context.getSharedPreferences("user", Context.MODE_PRIVATE).edit().apply();
95+
// Clear sticky bucketing
96+
context.deleteFile(String.format("optly-user-experiment-record-%s.json", MyApplication.PROJECT_ID));
97+
}
98+
})
99+
.around(new ExternalResource() {
100+
@Override
101+
protected void before() throws Throwable {
102+
super.before();
103+
104+
dataFileServiceIntent = new Intent(context, DataFileService.class);
105+
dataFileServiceIntent.putExtra(DataFileService.EXTRA_PROJECT_ID, MyApplication.PROJECT_ID);
106+
107+
eventIntentService = new Intent(context, EventIntentService.class);
108+
eventIntentService.putExtra(DataFileService.EXTRA_PROJECT_ID, MyApplication.PROJECT_ID);
109+
110+
Context applicationContext = context.getApplicationContext();
111+
ServiceScheduler.PendingIntentFactory pendingIntentFactory = new ServiceScheduler.PendingIntentFactory(applicationContext);
112+
AlarmManager alarmManager = (AlarmManager) applicationContext.getSystemService(Context.ALARM_SERVICE);
113+
serviceScheduler = new ServiceScheduler(alarmManager, pendingIntentFactory, LoggerFactory.getLogger(ServiceScheduler.class));
114+
}
115+
116+
@Override
117+
protected void after() {
118+
super.after();
119+
serviceScheduler.unschedule(dataFileServiceIntent);
120+
assertFalse(serviceScheduler.isScheduled(dataFileServiceIntent));
121+
assertFalse(serviceScheduler.isScheduled(eventIntentService));
122+
CountingIdlingResourceManager.clearEvents();
123+
}
124+
})
125+
.around(activityTestRule);
126+
127+
@Test
128+
public void experimentActivationForWhitelistUser() throws InterruptedException {
129+
// Check that the text was changed.
130+
// These tests are pointed at a real project.
131+
// The user 'test_user` is in the whitelist for variation 1 for experiment 0 and experiment 1.
132+
onView(withId(R.id.button_1))
133+
.check(matches(withText(context.getString(R.string.main_act_button_1_text_var_1))));
134+
135+
// Espresso will wait for Optimizely to start due to the registered idling resources
136+
onView(withId(R.id.text_view_1))
137+
.check(matches(withText(context.getString(R.string.main_act_text_view_1_var_1))));
138+
139+
assertTrue(serviceScheduler.isScheduled(dataFileServiceIntent));
140+
141+
onView(withId(R.id.button_1)) // withId(R.id.my_view) is a ViewMatcher
142+
.perform(click()); // click() is a ViewAction
143+
144+
onView(withId(R.id.text_view_1))
145+
.check(matches(withText(context.getString(R.string.secondary_frag_text_view_1_var_1))));
146+
147+
onView(withId(R.id.button_1)) // withId(R.id.my_view) is a ViewMatcher
148+
.perform(click()); // click() is a ViewAction
149+
150+
List<Pair<String, String>> events = CountingIdlingResourceManager.getEvents();
151+
assertTrue(events.size() == 6);
152+
Iterator<Pair<String, String>> iterator = events.iterator();
153+
while (iterator.hasNext()) {
154+
Pair<String, String> event = iterator.next();
155+
final String url = event.first;
156+
final String payload = event.second;
157+
if (url.equals("https://p13nlog.dz.optimizely.com/log/decision") && payload.contains("7676481120") && payload.contains("7661891902")
158+
|| url.equals("https://p13nlog.dz.optimizely.com/log/decision") && payload.contains("7651112186") && payload.contains("7674261140")
159+
|| url.equals("https://p13nlog.dz.optimizely.com/log/event") && payload.contains("experiment_0")
160+
|| url.equals("https://p13nlog.dz.optimizely.com/log/event") && payload.contains("experiment_1")
161+
|| url.equals("https://p13nlog.dz.optimizely.com/log/decision") && payload.contains("7680080715") && payload.contains("7685562539")
162+
|| url.equals("https://p13nlog.dz.optimizely.com/log/event") && payload.contains("experiment_2")) {
163+
iterator.remove();
164+
}
165+
}
166+
assertTrue(events.isEmpty());
167+
MyApplication myApplication = (MyApplication) activityTestRule.getActivity().getApplication();
168+
UserExperimentRecord userExperimentRecord = myApplication.getOptimizelyManager().getUserExperimentRecord();
169+
// Being in the white list should override user experiment record
170+
assertNull(userExperimentRecord.lookup("test_user", "experiment_0"));
171+
assertNull(userExperimentRecord.lookup("test_user", "experiment_1"));
172+
}
173+
}

0 commit comments

Comments
 (0)