Skip to content

Commit

Permalink
added basic tests (#173)
Browse files Browse the repository at this point in the history
* added basic espresso tests
  • Loading branch information
ngaruko authored May 13, 2021
1 parent 53b9646 commit e82a7cb
Show file tree
Hide file tree
Showing 5 changed files with 313 additions and 7 deletions.
18 changes: 18 additions & 0 deletions .github/workflows/build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -33,3 +33,21 @@ jobs:
with:
lane: build
options: '{ "flavor": "unbranded" }'

instrumentation-tests:
runs-on: macos-latest
steps:
- name: checkout
uses: actions/checkout@v2
- name: test unbranded
uses: reactivecircus/android-emulator-runner@v2
with:
api-level: 29
arch: x86
script: ./gradlew connectedUnbrandedWebviewDebugAndroidTest -Pabi=x86 --stacktrace
- name: run tests on gamma
uses: reactivecircus/android-emulator-runner@v2
with:
api-level: 29
arch: x86
script: ./gradlew connectedMedicmobilegammaWebviewDebugAndroidTest -Pabi=x86 --stacktrace
7 changes: 7 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -98,6 +98,13 @@ This release changes the way in which location data is collected to better align
3. Plug in your phone. Check it's detected with `adb devices`
4. Execute: `make` (will also push app unto phone)

# Testing - Instrumentation Tests (UI Tests)

1. Install Android SDK
2. Clone the repo
3. Plug in your phone. Check it's detected with `adb devices`
4. Execute: `./gradlew connected[Flavor]WebviewDebugAndroidTest` . Eg `./gradlew connectedUnbrandedWebviewDebugAndroidTest` or `./gradlew connectedMedicmobilegammaWebviewDebugAndroidTest`. At the moment we have tests only in these 2 flavors: unbranded and medicmobilegamma. To avoid failures running the tests, previous versions of the app should be uninstalled first, otherwise an `InstallException: INSTALL_FAILED_VERSION_DOWNGRADE` can make the tests to fail, and Android needs to have English as default language.

## Connecting to the server locally
Refer to the [cht-core Developer Guide](https://github.com/medic/cht-core/blob/master/DEVELOPMENT.md#testing-locally-with-devices).

Expand Down
30 changes: 23 additions & 7 deletions build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ buildscript {
jcenter()
}
dependencies {
classpath 'com.android.tools.build:gradle:4.1.2'
classpath 'com.android.tools.build:gradle:4.1.3'
classpath 'com.noveogroup.android:check:1.2.5'
}
}
Expand All @@ -35,7 +35,7 @@ repositories {
def simprintsApiKey, simprintsModuleId, simprintsUserId

def getVersionCode = {
int versionCode = 1
int versionCode = 2
if(System.env.CI == 'true' && System.env.RELEASE_VERSION && System.env.RELEASE_VERSION.startsWith('v')) {
def versionParts = System.env.RELEASE_VERSION.split(/[^0-9]+/)

Expand Down Expand Up @@ -72,6 +72,16 @@ android {
archivesBaseName = "${project.name}-${versionName}"
targetSdkVersion 29
// When upgrading targetSdkVersion, check that the app menu still works on newer devices.
//for espresso tests
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
//test user credentials
buildConfigField "String", "TEST_USERNAME", "\"${System.env.ANDROID_TEST_USERNAME}\""
buildConfigField "String", "TEST_PASSWORD", "\"${System.env.ANDROID_TEST_PASSWORD}\""
if(System.env.ANDROID_TEST_URL){
buildConfigField "String", "SERVER_URL", "\"${System.env.ANDROID_TEST_URL}\""
}else {
buildConfigField "String", "SERVER_URL", '"https://gamma-cht.dev.medicmobile.org"'
}
}

applicationVariants.all { variant ->
Expand Down Expand Up @@ -102,7 +112,7 @@ android {
buildConfigField "String", "SIMPRINTS_MODULE_ID", '"Medic Module ID"'
}

// Every APK requires a unique version code.
// Every APK requires a unique version code.
// So when compiling multiple APKS for the different ABIs, use the first digit
variant.outputs.each { output ->
def versionAugmentation = (output.getFilter(OutputFile.ABI) == 'arm64-v8a') ? 1 : 0;
Expand Down Expand Up @@ -315,12 +325,12 @@ android {

splits {
abi {
enable true
enable !project.hasProperty('abi')
reset()
include(
'armeabi-v7a',
'arm64-v8a',
//'x86', //--> uncomment to be able to deploy the app in Android 10+ virtual devices
'armeabi-v7a',
'arm64-v8a',
//'x86', //--> uncomment to be able to deploy the app in Android 10+ virtual devices
)
universalApk false
}
Expand All @@ -341,4 +351,10 @@ dependencies {
testImplementation 'junit:junit:4.12'
testImplementation 'com.google.android:android-test:4.1.1.4'
testImplementation 'org.robolectric:robolectric:4.3'
androidTestImplementation 'androidx.test.espresso:espresso-core:3.1.0'
androidTestImplementation 'androidx.test:runner:1.1.0'
androidTestImplementation 'androidx.test:rules:1.1.0'
androidTestImplementation 'androidx.test.espresso:espresso-web:3.1.0'
androidTestImplementation 'androidx.test:core:1.3.0'
androidTestImplementation 'androidx.test.ext:junit:1.1.1'
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,115 @@
package org.medicmobile.webapp.mobile;

import android.view.View;
import android.view.ViewGroup;
import android.view.ViewParent;
import androidx.test.espresso.DataInteraction;
import androidx.test.espresso.ViewInteraction;
import androidx.test.espresso.web.webdriver.DriverAtoms;
import androidx.test.espresso.web.webdriver.Locator;
import androidx.test.filters.LargeTest;
import androidx.test.internal.runner.junit4.AndroidJUnit4ClassRunner;
import androidx.test.rule.ActivityTestRule;
import org.hamcrest.Description;
import org.hamcrest.Matcher;
import org.hamcrest.TypeSafeMatcher;
import org.junit.FixMethodOrder;
import org.junit.Rule;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.junit.runners.MethodSorters;
import java.util.Locale;
import static androidx.test.espresso.Espresso.onData;
import static androidx.test.espresso.Espresso.onView;
import static androidx.test.espresso.action.ViewActions.click;
import static androidx.test.espresso.assertion.ViewAssertions.matches;
import static androidx.test.espresso.matcher.ViewMatchers.isDisplayed;
import static androidx.test.espresso.matcher.ViewMatchers.withId;
import static androidx.test.espresso.matcher.ViewMatchers.withParent;
import static androidx.test.espresso.web.assertion.WebViewAssertions.webContent;
import static androidx.test.espresso.web.assertion.WebViewAssertions.webMatches;
import static androidx.test.espresso.web.matcher.DomMatchers.hasElementWithId;
import static androidx.test.espresso.web.sugar.Web.onWebView;
import static androidx.test.espresso.web.webdriver.DriverAtoms.clearElement;
import static androidx.test.espresso.web.webdriver.DriverAtoms.findElement;
import static androidx.test.espresso.web.webdriver.DriverAtoms.getText;
import static androidx.test.espresso.web.webdriver.DriverAtoms.webClick;
import static org.hamcrest.Matchers.allOf;
import static org.hamcrest.Matchers.anything;
import static org.hamcrest.Matchers.containsString;

@LargeTest
@RunWith(AndroidJUnit4ClassRunner.class)
@FixMethodOrder(MethodSorters.NAME_ASCENDING)
public class LoginTests {

private static final String ERROR_INCORRECT = "Incorrect user name or password. Please try again.";

@Rule
public ActivityTestRule<SettingsDialogActivity> mActivityTestRule = new ActivityTestRule<>(SettingsDialogActivity.class);

@Test
public void testLoginScreen() throws Exception {
DataInteraction linearLayout = onData(anything())
.inAdapterView(allOf(withId(R.id.lstServers),
childAtPosition(
withId(android.R.id.content),
0)))
.atPosition(2);
linearLayout.perform(click());
Thread.sleep(10000);//TODO: use better ways to handle delays

ViewInteraction webView = onView(
allOf(withId(R.id.wbvMain),
withParent(allOf(withId(R.id.lytWebView),
withParent(withId(android.R.id.content)))),
isDisplayed()));
webView.check(matches(isDisplayed()));
onWebView()
.check(webContent(hasElementWithId("form")))
.withElement(findElement(Locator.ID, "locale"))
.check(webMatches(getText(), containsString("English")));
String[] codes = {"es", "en", "fr", "sw"};
for (String code : codes) {
onWebView().withElement(findElement(Locator.NAME, code))
.check(webMatches(getText(), containsString(getLanguage(code))));
}

//login form and errors
onWebView().withElement(findElement(Locator.ID, "user"))
.perform(clearElement())
.perform(DriverAtoms.webKeys("fakeName")) //to be created first
.withElement(findElement(Locator.ID, "password"))
.perform(clearElement())
.perform(DriverAtoms.webKeys("fake_password"))
.withElement(findElement(Locator.ID, "login"))
.perform(webClick());
Thread.sleep(4000);//TODO: use better ways to handle delays - takes longer with emulators
onWebView().withElement(findElement(Locator.CSS_SELECTOR, "p.error.incorrect"))
.check(webMatches(getText(), containsString(ERROR_INCORRECT)));
}

private String getLanguage(String code) {
Locale aLocale = new Locale(code);
return aLocale.getDisplayName();
}

private static Matcher<View> childAtPosition(
final Matcher<View> parentMatcher, final int position) {

return new TypeSafeMatcher<View>() {
@Override
public void describeTo(Description description) {
description.appendText("Child at position " + position + " in parent ");
parentMatcher.describeTo(description);
}

@Override
public boolean matchesSafely(View view) {
ViewParent parent = view.getParent();
return parent instanceof ViewGroup && parentMatcher.matches(parent)
&& view.equals(((ViewGroup) parent).getChildAt(position));
}
};
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,150 @@
package org.medicmobile.webapp.mobile;


import android.view.View;
import android.view.ViewGroup;
import android.view.ViewParent;
import androidx.test.espresso.DataInteraction;
import androidx.test.espresso.ViewInteraction;
import androidx.test.espresso.web.webdriver.DriverAtoms;
import androidx.test.espresso.web.webdriver.Locator;
import androidx.test.filters.LargeTest;
import androidx.test.rule.ActivityTestRule;
import org.hamcrest.Description;
import org.hamcrest.Matcher;
import org.hamcrest.TypeSafeMatcher;
import org.junit.FixMethodOrder;
import org.junit.Rule;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.junit.runners.MethodSorters;

import java.util.Locale;

import static androidx.test.espresso.Espresso.onData;
import static androidx.test.espresso.Espresso.onView;
import static androidx.test.espresso.Espresso.pressBack;
import static androidx.test.espresso.action.ViewActions.click;
import static androidx.test.espresso.action.ViewActions.closeSoftKeyboard;
import static androidx.test.espresso.action.ViewActions.replaceText;
import static androidx.test.espresso.assertion.ViewAssertions.matches;
import static androidx.test.espresso.matcher.ViewMatchers.hasErrorText;
import static androidx.test.espresso.matcher.ViewMatchers.isDisplayed;
import static androidx.test.espresso.matcher.ViewMatchers.withHint;
import static androidx.test.espresso.matcher.ViewMatchers.withId;
import static androidx.test.espresso.matcher.ViewMatchers.withParent;
import static androidx.test.espresso.matcher.ViewMatchers.withText;
import static androidx.test.espresso.web.assertion.WebViewAssertions.webContent;
import static androidx.test.espresso.web.assertion.WebViewAssertions.webMatches;
import static androidx.test.espresso.web.matcher.DomMatchers.hasElementWithId;
import static androidx.test.espresso.web.sugar.Web.onWebView;
import static androidx.test.espresso.web.webdriver.DriverAtoms.clearElement;
import static androidx.test.espresso.web.webdriver.DriverAtoms.findElement;
import static androidx.test.espresso.web.webdriver.DriverAtoms.getText;
import static androidx.test.espresso.web.webdriver.DriverAtoms.webClick;
import static org.hamcrest.Matchers.allOf;
import static org.hamcrest.Matchers.anything;
import static org.hamcrest.Matchers.containsString;
import androidx.test.internal.runner.junit4.AndroidJUnit4ClassRunner;


@LargeTest
@RunWith(AndroidJUnit4ClassRunner.class)
@FixMethodOrder(MethodSorters.NAME_ASCENDING)
public class SettingsDialogActivityTest {

private static final String WEBAPP_URL = "Webapp URL";
private static final String SERVER_ONE = "https://medic.github.io/atp";
private static final String SERVER_TWO = "https://gamma-cht.dev.medicmobile.org";
private static final String SERVER_THREE = "https://gamma.dev.medicmobile.org";
private static final String ERROR_INCORRECT = "Incorrect user name or password. Please try again.";

@Rule
public ActivityTestRule<SettingsDialogActivity> mActivityTestRule = new ActivityTestRule<>(SettingsDialogActivity.class);

@Test
public void serverSelectionScreenIsDisplayed() {
onView(withText("Medic Mobile")).check(matches(isDisplayed()));
onView(withText("Custom")).check(matches(isDisplayed()));
onView(withId(R.id.lstServers)).check(matches(isDisplayed()));

onView(withText(SERVER_ONE)).check(matches(isDisplayed()));
onView(withText(SERVER_TWO)).check(matches(isDisplayed()));
onView(withText(SERVER_THREE)).check(matches(isDisplayed()));

onView(withText("Custom")).perform(click());
ViewInteraction textAppUrl = onView(withId(R.id.txtAppUrl));
textAppUrl.check(matches(withHint(WEBAPP_URL)));

textAppUrl.perform(replaceText("something"), closeSoftKeyboard());
onView(withId(R.id.btnSaveSettings)).perform(click());
textAppUrl.check(matches(hasErrorText("must be a valid URL")));
pressBack();

}

@Test
public void testLoginScreen() throws Exception {
DataInteraction linearLayout = onData(anything())
.inAdapterView(allOf(withId(R.id.lstServers),
childAtPosition(
withId(android.R.id.content),
0)))
.atPosition(2);
linearLayout.perform(click());
Thread.sleep(10000);//TODO: use better ways to handle delays

ViewInteraction webView = onView(
allOf(withId(R.id.wbvMain),
withParent(allOf(withId(R.id.lytWebView),
withParent(withId(android.R.id.content)))),
isDisplayed()));
webView.check(matches(isDisplayed()));
onWebView()
.check(webContent(hasElementWithId("form")))
.withElement(findElement(Locator.ID, "locale"))
.check(webMatches(getText(), containsString("English")));
String[] codes = {"es", "en", "fr", "sw"};
for (String code : codes) {
onWebView().withElement(findElement(Locator.NAME, code))
.check(webMatches(getText(), containsString(getLanguage(code))));
}

//login form and errors
onWebView().withElement(findElement(Locator.ID, "user"))
.perform(clearElement())
.perform(DriverAtoms.webKeys("fakename")) //to be created first
.withElement(findElement(Locator.ID, "password"))
.perform(clearElement())
.perform(DriverAtoms.webKeys("fake_password"))
.withElement(findElement(Locator.ID, "login"))
.perform(webClick());
Thread.sleep(4000);//TODO: use better ways to handle delays - takes longer with emulators
onWebView().withElement(findElement(Locator.CSS_SELECTOR, "p.error.incorrect"))
.check(webMatches(getText(), containsString(ERROR_INCORRECT)));
}

private String getLanguage(String code) {
Locale aLocale = new Locale(code);
return aLocale.getDisplayName();
}

private static Matcher<View> childAtPosition(
final Matcher<View> parentMatcher, final int position) {

return new TypeSafeMatcher<View>() {
@Override
public void describeTo(Description description) {
description.appendText("Child at position " + position + " in parent ");
parentMatcher.describeTo(description);
}

@Override
public boolean matchesSafely(View view) {
ViewParent parent = view.getParent();
return parent instanceof ViewGroup && parentMatcher.matches(parent)
&& view.equals(((ViewGroup) parent).getChildAt(position));
}
};
}
}

0 comments on commit e82a7cb

Please sign in to comment.