From 5fd65e8f7254bc542d21eac62f745ebd50667cdd Mon Sep 17 00:00:00 2001 From: Martin Harriman Date: Thu, 20 Feb 2020 16:16:19 -0800 Subject: [PATCH] Android unit test Tests that one can instantiate the Java class, both with and without ConnectivityManager (API 24). Tests some behavior in running a request. --- .github/workflows/build-android.yml | 17 +- lib/android_build/maesdk/build.gradle | 93 +++++----- .../events/{ariasdk => }/httpClient.java | 40 ++-- .../applications/events/EventsUnitTest.java | 174 ++++++++++++++++++ .../applications/events/ExampleUnitTest.java | 17 -- lib/http/HttpClient_Android.cpp | 2 +- 6 files changed, 261 insertions(+), 82 deletions(-) rename lib/android_build/maesdk/src/main/java/com/microsoft/applications/events/{ariasdk => }/httpClient.java (89%) create mode 100644 lib/android_build/maesdk/src/test/java/com/microsoft/applications/events/EventsUnitTest.java delete mode 100644 lib/android_build/maesdk/src/test/java/com/microsoft/applications/events/ExampleUnitTest.java diff --git a/.github/workflows/build-android.yml b/.github/workflows/build-android.yml index 4e2739b18..04894386c 100644 --- a/.github/workflows/build-android.yml +++ b/.github/workflows/build-android.yml @@ -3,8 +3,13 @@ name: Build for Android on: push: branches: - maharrim/* - + - master + - release/* + - buildme/* + - maharrim/* + pull_request: + branches: + - master jobs: build: runs-on: windows-latest @@ -21,7 +26,13 @@ jobs: run: | cd "$Env:GITHUB_WORKSPACE\lib\android_build" .\gradlew.bat assemble - dir maesdk\build\outputs + .\gradlew.bat maesdk:test + - name: Upload Reports + if: failure() + uses: actions/upload-artifact@v1 + with: + name: reports + path: lib\android_build\maesdk\build\reports - name: Upload Artifacts uses: actions/upload-artifact@v1 with: diff --git a/lib/android_build/maesdk/build.gradle b/lib/android_build/maesdk/build.gradle index 9c31da1b2..0a291c437 100644 --- a/lib/android_build/maesdk/build.gradle +++ b/lib/android_build/maesdk/build.gradle @@ -1,46 +1,47 @@ -apply plugin: 'com.android.library' - -android { - compileSdkVersion 29 - buildToolsVersion "29.0.2" - - - defaultConfig { - minSdkVersion 19 - targetSdkVersion 29 - versionCode 1 - versionName "1.0" - - testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" - consumerProguardFiles 'consumer-rules.pro' - externalNativeBuild { - cmake { - // Passes optional arguments to CMake. - arguments "-DANDROID_STL=c++_shared" - } - } - } - - buildTypes { - release { - minifyEnabled false - proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro' - } - } - - externalNativeBuild { - cmake { - path "src/main/cpp/CMakeLists.txt" - version "3.16.2" - } - } -} - -dependencies { - implementation fileTree(dir: 'libs', include: ['*.jar']) - - implementation 'androidx.appcompat:appcompat:1.1.0' - testImplementation 'junit:junit:4.12' - androidTestImplementation 'androidx.test.ext:junit:1.1.1' - androidTestImplementation 'androidx.test.espresso:espresso-core:3.2.0' -} +apply plugin: 'com.android.library' + +android { + compileSdkVersion 29 + buildToolsVersion "29.0.2" + + + defaultConfig { + minSdkVersion 19 + targetSdkVersion 29 + versionCode 1 + versionName "1.0" + + testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" + consumerProguardFiles 'consumer-rules.pro' + externalNativeBuild { + cmake { + // Passes optional arguments to CMake. + arguments "-DANDROID_STL=c++_shared" + } + } + } + + buildTypes { + release { + minifyEnabled false + proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro' + } + } + + externalNativeBuild { + cmake { + path "src/main/cpp/CMakeLists.txt" + version "3.16.2" + } + } +} + +dependencies { + implementation fileTree(dir: 'libs', include: ['*.jar']) + + implementation 'androidx.appcompat:appcompat:1.1.0' + testImplementation 'junit:junit:4.12' + testImplementation 'org.mockito:mockito-inline:3.2.4' + androidTestImplementation 'androidx.test.ext:junit:1.1.1' + androidTestImplementation 'androidx.test.espresso:espresso-core:3.2.0' +} diff --git a/lib/android_build/maesdk/src/main/java/com/microsoft/applications/events/ariasdk/httpClient.java b/lib/android_build/maesdk/src/main/java/com/microsoft/applications/events/httpClient.java similarity index 89% rename from lib/android_build/maesdk/src/main/java/com/microsoft/applications/events/ariasdk/httpClient.java rename to lib/android_build/maesdk/src/main/java/com/microsoft/applications/events/httpClient.java index e7451dae2..966616f6d 100644 --- a/lib/android_build/maesdk/src/main/java/com/microsoft/applications/events/ariasdk/httpClient.java +++ b/lib/android_build/maesdk/src/main/java/com/microsoft/applications/events/httpClient.java @@ -75,9 +75,10 @@ final public void onCapabilitiesChanged(Network network, NetworkCapabilities net class Request implements Runnable { - Request(String url, String method, byte[] body, String request_id, int[] header_length, byte[] header_buffer) + Request(httpClient parent, String url, String method, byte[] body, String request_id, int[] header_length, byte[] header_buffer) throws java.net.MalformedURLException, java.io.IOException { - m_url = new URL(url); + m_parent = parent; + m_url = parent.newUrl(url); m_connection = (HttpURLConnection) m_url.openConnection(); m_connection.setRequestMethod(method); m_body = body; @@ -150,21 +151,14 @@ public void run() { } finally { m_connection.disconnect(); } - dispatchCallback(m_request_id, response, headerArray, body); - } - - public void dispatchCallback(String id, int response, Object[] headers, byte[] body) { - // this stub makes it easier to mock this method in - // Java unit tests. - nativeDispatchCallback(id, response, headers, body); + m_parent.dispatchCallback(m_request_id, response, headerArray, body); } - public native void nativeDispatchCallback(String id, int response, Object[] headers, byte[] body); - private URL m_url; private HttpURLConnection m_connection; private byte[] m_body = {}; public String m_request_id; + protected httpClient m_parent; } public class httpClient { @@ -174,10 +168,10 @@ public httpClient(android.content.Context context) { m_context = context; String path = System.getProperty("java.io.tmpdir"); setCacheFilePath(path); - m_executor = Executors.newFixedThreadPool(MAX_HTTP_THREADS); + m_executor = createExecutor(); createClientInstance(); // We need API 24 to follow changes in network status - if (Build.VERSION.SDK_INT >= 24) { + if (hasConnectivityManager()) { m_connectivityManager = (ConnectivityManager) context.getSystemService(Context.CONNECTIVITY_SERVICE); boolean is_metered = m_connectivityManager.isActiveNetworkMetered(); m_callback = new ConnectivityCallback(this, is_metered); @@ -190,6 +184,16 @@ public httpClient(android.content.Context context) { m_power_receiver.onReceive(context, status); } + protected ExecutorService createExecutor() + { + return Executors.newFixedThreadPool(MAX_HTTP_THREADS); + } + + protected boolean hasConnectivityManager() + { + return Build.VERSION.SDK_INT >= 24; + } + public void finalize() { if (m_callback != null) { m_connectivityManager.unregisterNetworkCallback(m_callback); @@ -201,6 +205,11 @@ public void finalize() { m_executor.shutdown(); } + public URL newUrl(String url) throws java.net.MalformedURLException + { + return new URL(url); + } + public native void createClientInstance(); public native void deleteClientInstance(); @@ -211,12 +220,13 @@ public void finalize() { public native void onPowerChange(boolean isCharging, boolean isLow); + public native void dispatchCallback(String id, int response, Object[] headers, byte[] body); + public FutureTask createTask(String url, String method, byte[] body, String request_id, int[] header_index, byte[] header_buffer) { try { - Request r = new Request(url, method, body, request_id, header_index, header_buffer); + Request r = new Request(this, url, method, body, request_id, header_index, header_buffer); FutureTask t = new FutureTask(r, true); - m_executor.execute(t); return t; } catch (Exception e) { return null; diff --git a/lib/android_build/maesdk/src/test/java/com/microsoft/applications/events/EventsUnitTest.java b/lib/android_build/maesdk/src/test/java/com/microsoft/applications/events/EventsUnitTest.java new file mode 100644 index 000000000..ab907b482 --- /dev/null +++ b/lib/android_build/maesdk/src/test/java/com/microsoft/applications/events/EventsUnitTest.java @@ -0,0 +1,174 @@ +package com.microsoft.applications.events; + +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import static org.mockito.Mockito.*; +import org.mockito.junit.MockitoJUnitRunner; + +import android.content.BroadcastReceiver; +import android.content.IntentFilter; +import android.content.Context; +import android.content.Intent; +import android.net.ConnectivityManager; +import android.os.BatteryManager; + +import java.io.ByteArrayInputStream; +import java.io.InputStream; +import java.io.OutputStream; +import java.net.HttpURLConnection; +import java.net.URL; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.FutureTask; +import java.util.List; +import java.util.Map; +import java.util.TreeMap; +import java.util.Vector; + +import com.microsoft.applications.events.httpClient; + +import static org.junit.Assert.*; + +/** + * Example local unit test, which will execute on the development machine (host). + * + * @see Testing documentation + */ +@RunWith(MockitoJUnitRunner.class) +public class EventsUnitTest { + @Mock + Context mockContext; + + @Mock + Intent mockIntent; + + @Mock + ConnectivityManager mockManager; + + @Mock + URL mockUrl; + + @Mock + HttpURLConnection mockConnection; + + @Mock + OutputStream mockBodyStream; + + @Mock + ExecutorService mockExecutor; + + class Stubby extends httpClient { + + Stubby(Context context) throws java.io.IOException { + super(context); + } + + @Override + protected boolean hasConnectivityManager() { + return false; + } + + @Override + public void createClientInstance() { + + } + + @Override + public void deleteClientInstance() { + + } + + @Override + public void setCacheFilePath(String path) { + + } + + @Override + public void onCostChange(boolean isMetered) { + + } + + @Override + public void onPowerChange(boolean isCharging, boolean isLow) { + + } + + @Override + public void dispatchCallback(String id, int response, Object[] headers, byte[] body) { + assertNotNull(id); + } + + @Override + public URL newUrl(String url) throws java.net.MalformedURLException { + return mockUrl; + } + + @Override + protected ExecutorService createExecutor() + { + return mockExecutor; + } + } + + class StubbyAllow extends Stubby { + StubbyAllow(Context context) throws java.io.IOException { + super(context); + } + + @Override + protected boolean hasConnectivityManager() { + return true; + } + } + + @Test + public void canInstantiate() throws java.io.IOException { + when(mockContext.registerReceiver(isA(BroadcastReceiver.class), isA(IntentFilter.class))).thenReturn(mockIntent); + Stubby stubs = new Stubby(mockContext); + /* Stubby should not attempt to access the CONNECTIVITY_SERVICE */ + verify(mockContext, times(0)).getSystemService(Context.CONNECTIVITY_SERVICE); + verify(mockContext, times(1)).registerReceiver(isA(BroadcastReceiver.class), isA(IntentFilter.class)); + verify(mockIntent, times(1)).getIntExtra(BatteryManager.EXTRA_STATUS, -1); + } + + @Test + public void canInstantiateWithConnectivity() throws java.io.IOException { + when(mockContext.registerReceiver(isA(BroadcastReceiver.class), isA(IntentFilter.class))).thenReturn(mockIntent); + when(mockContext.getSystemService(Context.CONNECTIVITY_SERVICE)).thenReturn(mockManager); + StubbyAllow stubs = new StubbyAllow(mockContext); + verify(mockContext, times(1)).getSystemService(Context.CONNECTIVITY_SERVICE); + verify(mockManager, times(1)).isActiveNetworkMetered(); + verify(mockManager, times(1)).registerDefaultNetworkCallback(isA(android.net.ConnectivityManager.NetworkCallback.class)); + } + + @Test + public void canCreateTask() throws java.io.IOException { + final List someValue = new Vector(); + someValue.add("bar"); + final Map> headerMap = new TreeMap>(); + headerMap.put("foo", someValue); + String body_string = "fred"; + byte[] body_bytes = body_string.getBytes(); + final InputStream bodyStream = new ByteArrayInputStream(body_bytes); + + when(mockUrl.openConnection()).thenReturn(mockConnection); + when(mockConnection.getOutputStream()).thenReturn(mockBodyStream); + when(mockConnection.getHeaderFields()).thenReturn(headerMap); + when(mockConnection.getResponseCode()).thenReturn(200); + when(mockConnection.getInputStream()).thenReturn(bodyStream); + when(mockContext.registerReceiver(isA(BroadcastReceiver.class), isA(IntentFilter.class))).thenReturn(mockIntent); + + Stubby stubs = new Stubby(mockContext); + final String url = "https://www.contoso.com"; + final String method = "POST"; + final byte[] body = {0, 1, 2}; + final String request_id = "Fred The Request"; + final int[] header_index = {4, 3}; + final byte[] header_buffer = {0, 1, 2, 3, 7, 8, 9}; + FutureTask task = stubs.createTask( + url, method, body, request_id, header_index, header_buffer + ); + assertNotNull(task); + task.run(); + } +} \ No newline at end of file diff --git a/lib/android_build/maesdk/src/test/java/com/microsoft/applications/events/ExampleUnitTest.java b/lib/android_build/maesdk/src/test/java/com/microsoft/applications/events/ExampleUnitTest.java deleted file mode 100644 index 47ab078b9..000000000 --- a/lib/android_build/maesdk/src/test/java/com/microsoft/applications/events/ExampleUnitTest.java +++ /dev/null @@ -1,17 +0,0 @@ -package com.microsoft.applications.events; - -import org.junit.Test; - -import static org.junit.Assert.*; - -/** - * Example local unit test, which will execute on the development machine (host). - * - * @see Testing documentation - */ -public class ExampleUnitTest { - @Test - public void addition_isCorrect() { - assertEquals(4, 2 + 2); - } -} \ No newline at end of file diff --git a/lib/http/HttpClient_Android.cpp b/lib/http/HttpClient_Android.cpp index 854df9ec5..8155371a3 100644 --- a/lib/http/HttpClient_Android.cpp +++ b/lib/http/HttpClient_Android.cpp @@ -438,7 +438,7 @@ extern "C" JNIEXPORT void JNICALL -Java_com_microsoft_applications_events_Request_nativeDispatchCallback( +Java_com_microsoft_applications_events_httpClient_dispatchCallback( JNIEnv* env, jobject /* this */, jstring id,