Add End to End Tests
Sets up End to End Tests as described in
The implementation uses Firebase Test Lab instead of running emulators.
MODIFY
.circleci/config.yml
.circleci/config.yml
@@ -3,21 +3,41 @@ jobs:
build:
working_directory: ~/analytics-android
docker:
- - image: circleci/android:api-26-alpha
+ - image: circleci/android:api-27-alpha
environment:
JVM_OPTS: -Xmx3200m
CIRCLE_JDK_VERSION: oraclejdk8
steps:
- checkout
- restore_cache:
- key: jars-{{ checksum "build.gradle" }}-{{ checksum "analytics/build.gradle" }}
+ key: jars-{{ checksum "build.gradle" }}-{{ checksum "analytics/build.gradle" }}-{{ checksum "analytics-samples/analytics-sample/build.gradle" }}
+ - run:
+ name: Accept licenses
+ command: yes | sdkmanager --licenses || true
+ - run:
+ name: Copy E2E Test Credentials
+ command: |
+ if [ "${RUN_E2E_TESTS}" == "true" ]; then
+ echo "RUNSCOPE_TOKEN=\"${RUNSCOPE_TOKEN}\"" >> "analytics-samples/analytics-sample/e2e_test.properties"
+ fi
- run:
name: Build
- command: ./gradlew checkFmt build
+ command: ./gradlew checkFmt build assembleAndroidTest
+ - run:
+ name: Run E2E Test
+ command: |
+ if [ "${RUN_E2E_TESTS}" == "true" ]; then
+ echo "${FIREBASE_TOKEN}" >> "secret.json"
+ curl -O https://dl.google.com/dl/cloudsdk/release/google-cloud-sdk.zip
+ unzip -qq google-cloud-sdk.zip
+ ./google-cloud-sdk/bin/gcloud config set project analytics-android-e2e-test
+ ./google-cloud-sdk/bin/gcloud auth activate-service-account --key-file secret.json
+ ./google-cloud-sdk/bin/gcloud firebase test android run --app analytics-samples/analytics-sample/build/outputs/apk/debug/analytics-sample-debug.apk --test analytics-samples/analytics-sample/build/outputs/apk/androidTest/debug/analytics-sample-debug-androidTest.apk --device-ids sailfish,NexusLowRes,hammerhead --os-version-ids 19,21,22,23,24,25,26,27 --locales en,fr --orientations portrait
+ fi
- save_cache:
paths:
- ~/.gradle
- key: jars-{{ checksum "build.gradle" }}-{{ checksum "analytics/build.gradle" }}
+ key: jars-{{ checksum "build.gradle" }}-{{ checksum "analytics/build.gradle" }}-{{ checksum "analytics-samples/analytics-sample/build.gradle" }}
- run:
name: Upload code coverage reports
command: bash <(curl -s https://codecov.io/bash)

MODIFY
.gitignore
.gitignore
@@ -21,6 +21,7 @@ build/
# Local configuration file (sdk path, etc)
local.properties
+e2e_test.properties
# Proguard folder generated by Eclipse
proguard/

MODIFY
analytics-samples/analytics-sample/build.gradle
analytics-samples/analytics-sample/build.gradle
@@ -3,11 +3,30 @@ apply plugin: 'com.f2prateek.javafmt'
apply from: rootProject.file('gradle/android.gradle')
+def RUNSCOPE_TOKEN = ""
+def RUN_E2E_TESTS = false
+
+// e2e_test.properties is a file with a single line: RUNSCOPE_TOKEN="<token>"
+def e2eTestPropertiesFile = file('e2e_test.properties')
+if (e2eTestPropertiesFile.exists()) {
+ def props = new Properties()
+ file(e2eTestPropertiesFile).withInputStream { stream -> props.load(stream) }
+ RUNSCOPE_TOKEN = props.getProperty("RUNSCOPE_TOKEN")
+ RUN_E2E_TESTS = true
+}
+
android {
lintOptions {
// Since this is just a sample app, it's ok to ignore these warnings.
disable 'IconMissingDensityFolder', 'GoogleAppIndexingWarning', 'AllowBackup'
}
+
+ defaultConfig {
+ testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner"
+
+ buildConfigField 'String', 'RUNSCOPE_TOKEN', "$RUNSCOPE_TOKEN"
+ buildConfigField 'boolean', 'RUN_E2E_TESTS', "$RUN_E2E_TESTS"
+ }
}
dependencies {
@@ -17,4 +36,12 @@ dependencies {
implementation 'uk.co.chrisjenx:calligraphy:2.2.0'
implementation 'com.jakewharton:butterknife:8.6.0'
annotationProcessor 'com.jakewharton:butterknife-compiler:8.6.0'
+
+ androidTestImplementation 'com.squareup.retrofit2:retrofit:2.3.0'
+ androidTestImplementation 'com.squareup.retrofit2:converter-moshi:2.3.0'
+
+ androidTestImplementation 'com.android.support:support-annotations:27.0.2'
+ androidTestImplementation 'com.android.support.test:runner:1.0.1'
+ androidTestImplementation 'com.android.support.test:rules:1.0.1'
+ androidTestImplementation 'com.segment.backo:backo:1.0.0'
}

ADD
analytics-samples/analytics-sample/src/androidTest/java/com/segment/analytics/E2ETest.java
+package com.segment.analytics;
+
+
+import static org.junit.Assert.fail;
+
+import android.support.test.InstrumentationRegistry;
+import android.support.test.filters.RequiresDevice;
+import android.support.test.rule.ActivityTestRule;
+import android.support.test.runner.AndroidJUnit4;
+import com.segment.analytics.runscope.MessageResponse;
+import com.segment.analytics.runscope.MessagesResponse;
+import com.segment.analytics.runscope.RunscopeService;
+import com.segment.analytics.sample.MainActivity;
+import com.segment.analytics.sample.test.BuildConfig;
+import com.segment.backo.Backo;
+import java.io.IOException;
+import java.util.UUID;
+import java.util.concurrent.TimeUnit;
+import okhttp3.Interceptor;
+import okhttp3.OkHttpClient;
+import org.junit.Before;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import retrofit2.Response;
+import retrofit2.Retrofit;
+import retrofit2.converter.moshi.MoshiConverterFactory;
+
+/**
+ * An end to end to test that sends events to a Segment source, and verifies that a webhook
+ * connected to the source connected to the source (configured manually via the app) is able to
+ * receive the data sent by this library.
+ *
+ * See https://paper.dropbox.com/doc/Libraries-End-to-End-Tests-ESEakc3LxFrqcHz69AmyN for details.
+ */
+@RunWith(AndroidJUnit4.class)
+public class E2ETest {
+
+ @Rule
+ public final ActivityTestRule<MainActivity> activityActivityTestRule =
+ new ActivityTestRule<>(MainActivity.class);
+
+ /**
+ * Write key for the Segment project to send data to.
+ * https://app.segment.com/segment-libraries/sources/analytics_android_e2e_test/overview
+ */
+ private static final String SEGMENT_WRITE_KEY = "OAtgAHjkAD5MP31srDe9wiBjpvcXC8De";
+ /**
+ * Runscope bucket that is connect to the Segment project.
+ * https://www.runscope.com/radar/uy22axz4sdb8
+ */
+ private static final String RUNSCOPE_BUCKET = "uy22axz4sdb8";
+ // Token to read data from the Runscope bucket.
+ private static final String RUNSCOPE_TOKEN = BuildConfig.RUNSCOPE_TOKEN;
+
+ private static final Backo BACKO = Backo.builder()
+ .base(TimeUnit.SECONDS, 1)
+ .cap(TimeUnit.SECONDS, 5)
+ .build();
+
+ private Analytics analytics;
+ private RunscopeService runscopeService;
+
+ @Before
+ public void setup() {
+ analytics = new Analytics.Builder(activityActivityTestRule.getActivity(), SEGMENT_WRITE_KEY)
+ .build();
+
+ runscopeService = new Retrofit.Builder()
+ .baseUrl("https://api.runscope.com")
+ .addConverterFactory(MoshiConverterFactory.create())
+ .client(new OkHttpClient.Builder()
+ .addNetworkInterceptor(new Interceptor() {
+ @Override
+ public okhttp3.Response intercept(Chain chain) throws IOException {
+ return chain.proceed(chain.request()
+ .newBuilder()
+ .addHeader("Authorization", "Bearer " + RUNSCOPE_TOKEN)
+ .build());
+ }
+ })
+ .build())
+ .build()
+ .create(RunscopeService.class);
+ }
+
+ @Test
+ public void track() throws InterruptedException, IOException {
+ // Skip the test if End to End tests are disabled (e.g. contributor PRs).
+ if (!BuildConfig.RUN_E2E_TESTS) {
+ return;
+ }
+
+ final String uuid = UUID.randomUUID().toString();
+ analytics.track("E2E Test", new Properties().putValue("id", uuid));
+ analytics.flush();
+
+ for (int i = 0; i < 10; i++) {
+ BACKO.sleep(i);
+
+ if (hasMatchingRequest(uuid)) {
+ return;
+ }
+ }
+
+ fail("did not find message with id: " + uuid);
+ }
+
+ /** Returns {@code true} if a message with the provided ID is found in Runscope. */
+ @SuppressWarnings("ConstantConditions")
+ private boolean hasMatchingRequest(String id) throws IOException {
+ Response<MessagesResponse> messagesResponse = runscopeService
+ .messages(RUNSCOPE_BUCKET)
+ .execute();
+
+ for (MessagesResponse.Message message : messagesResponse.body().data) {
+ Response<MessageResponse> messageResponse = runscopeService
+ .message(RUNSCOPE_BUCKET, message.uuid)
+ .execute();
+
+ // TODO: Deserialize into Segment message and check against properties.
+ if (messageResponse.body().data.request.body.contains(id)) {
+ return true;
+ }
+ }
+
+ return false;
+ }
+}

ADD
analytics-samples/analytics-sample/src/androidTest/java/com/segment/analytics/runscope/MessageResponse.java
+package com.segment.analytics.runscope;
+
+public class MessageResponse {
+ public static class Message {
+ public static class Request {
+ public String body;
+ }
+
+ public Request request;
+ }
+
+ public Message data;
+}

ADD
analytics-samples/analytics-sample/src/androidTest/java/com/segment/analytics/runscope/MessagesResponse.java
+package com.segment.analytics.runscope;
+
+import java.util.List;
+
+public class MessagesResponse {
+ public static class Message {
+ public String uuid;
+ }
+
+ public List<Message> data;
+}

ADD
analytics-samples/analytics-sample/src/androidTest/java/com/segment/analytics/runscope/RunscopeService.java
+package com.segment.analytics.runscope;
+
+import retrofit2.Call;
+import retrofit2.http.GET;
+import retrofit2.http.Path;
+
+public interface RunscopeService {
+ @GET("buckets/{bucket}/messages")
+ Call<MessagesResponse> messages(@Path("bucket") String bucket);
+
+ @GET("buckets/{bucket}/messages/{message}")
+ Call<MessageResponse> message(@Path("bucket") String bucket,
+ @Path("message") String message);
+}

MODIFY
analytics/src/test/java/com/segment/analytics/ClientTest.java
analytics/src/test/java/com/segment/analytics/ClientTest.java
@@ -12,7 +12,6 @@ import com.segment.analytics.internal.Private;
import com.squareup.okhttp.mockwebserver.MockResponse;
import com.squareup.okhttp.mockwebserver.RecordedRequest;
import com.squareup.okhttp.mockwebserver.rule.MockWebServerRule;
-
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.InputStream;

Add End to End Tests

Sets up End to End Tests as described in https://paper.dropbox.com/doc/Libraries-End-to-End-Tests-ESEakc3LxFrqcHz69AmyN.

The implementation uses Firebase Test Lab instead of running emulators.