From b1e812314f606f14f6b684647fa3d05dd7e1e9c4 Mon Sep 17 00:00:00 2001 From: Amine Date: Sun, 11 Jun 2017 12:04:39 +0200 Subject: [PATCH] Added tests and testing in ci. --- app/build.gradle | 30 ++++ .../readerforselfoss/AddSourceEspressoTest.kt | 4 + .../HomeActivityEspressoTest.kt | 123 +++++++++++++ .../IntroActivityEspressoTest.kt | 121 +++++++++++++ .../LoginActivityEspressoTest.kt | 164 ++++++++++++++++++ .../MainActivityEspressoTest.kt | 73 ++++++++ .../apps/amine/bou/readerforselfoss/Utils.kt | 31 ++++ app/src/main/res/layout/activity_login.xml | 1 + app/src/main/res/menu/home_menu.xml | 7 +- build.gradle | 12 ++ circle.yml | 15 +- 11 files changed, 576 insertions(+), 5 deletions(-) create mode 100644 app/src/androidTest/java/apps/amine/bou/readerforselfoss/AddSourceEspressoTest.kt create mode 100644 app/src/androidTest/java/apps/amine/bou/readerforselfoss/HomeActivityEspressoTest.kt create mode 100644 app/src/androidTest/java/apps/amine/bou/readerforselfoss/IntroActivityEspressoTest.kt create mode 100644 app/src/androidTest/java/apps/amine/bou/readerforselfoss/LoginActivityEspressoTest.kt create mode 100644 app/src/androidTest/java/apps/amine/bou/readerforselfoss/MainActivityEspressoTest.kt create mode 100644 app/src/androidTest/java/apps/amine/bou/readerforselfoss/Utils.kt diff --git a/app/build.gradle b/app/build.gradle index fdf141f..d120aa3 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -35,6 +35,9 @@ android { disable 'InvalidPackage' } vectorDrawables.useSupportLibrary = true + + // tests + testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner" } buildTypes { release { @@ -42,6 +45,11 @@ android { proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro' } + debug { + buildConfigField "String", "LOGIN_URL", appLoginUrl + buildConfigField "String", "LOGIN_USERNAME", appLoginUsername + buildConfigField "String", "LOGIN_PASSWORD", appLoginPassword + } } flavorDimensions "build" productFlavors { @@ -59,6 +67,15 @@ android { } dependencies { + // Testing + androidTestCompile 'com.android.support.test.espresso:espresso-core:2.2.2' + androidTestCompile 'com.android.support.test:runner:0.5' + // Espresso-contrib for DatePicker, RecyclerView, Drawer actions, Accessibility checks, CountingIdlingResource + androidTestCompile 'com.android.support.test.espresso:espresso-contrib:2.2.2' + // Espresso-intents for validation and stubbing of Intents + androidTestCompile 'com.android.support.test.espresso:espresso-intents:2.2.2' + + compile fileTree(dir: 'libs', include: ['*.jar']) compile "org.jetbrains.kotlin:kotlin-stdlib-jre7:$kotlin_version" @@ -126,6 +143,7 @@ apply plugin: 'com.google.gms.google-services' afterEvaluate { initFabricPropertiesIfNeeded() + initAppLoginPropertiesIfNeeded() } def initFabricPropertiesIfNeeded() { @@ -137,4 +155,16 @@ def initFabricPropertiesIfNeeded() { entry(key: "apiKey", value: crashlyticsdemoApikey) } } +} + +def initAppLoginPropertiesIfNeeded() { + def propertiesFile = file(System.getProperty("user.home") + '/.gradle/gradle.properties') + if (!propertiesFile.exists()) { + def commentMessage = "This is autogenerated local property from system environment to prevent key to be committed to source control." + ant.propertyfile(file: System.getProperty("user.home") + "/.gradle/gradle.properties", comment: commentMessage) { + entry(key: "appLoginUrl", value: System.getProperty("appLoginUrl")) + entry(key: "appLoginUsername", value: System.getProperty("appLoginUsername")) + entry(key: "appLoginPassword", value: System.getProperty("appLoginPassword")) + } + } } \ No newline at end of file diff --git a/app/src/androidTest/java/apps/amine/bou/readerforselfoss/AddSourceEspressoTest.kt b/app/src/androidTest/java/apps/amine/bou/readerforselfoss/AddSourceEspressoTest.kt new file mode 100644 index 0000000..a54c316 --- /dev/null +++ b/app/src/androidTest/java/apps/amine/bou/readerforselfoss/AddSourceEspressoTest.kt @@ -0,0 +1,4 @@ +package apps.amine.bou.readerforselfoss + + +// TODO: test source adding \ No newline at end of file diff --git a/app/src/androidTest/java/apps/amine/bou/readerforselfoss/HomeActivityEspressoTest.kt b/app/src/androidTest/java/apps/amine/bou/readerforselfoss/HomeActivityEspressoTest.kt new file mode 100644 index 0000000..2509b3d --- /dev/null +++ b/app/src/androidTest/java/apps/amine/bou/readerforselfoss/HomeActivityEspressoTest.kt @@ -0,0 +1,123 @@ +package apps.amine.bou.readerforselfoss + +import android.content.Context +import android.content.Intent +import android.support.test.InstrumentationRegistry +import android.support.test.espresso.Espresso.onView +import android.support.test.espresso.Espresso.openActionBarOverflowOrOptionsMenu +import android.support.test.espresso.action.ViewActions.* +import android.support.test.espresso.assertion.ViewAssertions.matches +import android.support.test.espresso.contrib.DrawerActions +import android.support.test.espresso.intent.Intents +import android.support.test.espresso.intent.Intents.intended +import android.support.test.espresso.intent.Intents.times +import android.support.test.espresso.intent.matcher.IntentMatchers.hasComponent +import android.support.test.espresso.matcher.ViewMatchers.* +import android.support.test.rule.ActivityTestRule +import android.support.test.runner.AndroidJUnit4 +import android.view.KeyEvent + +import com.mikepenz.aboutlibraries.ui.LibsActivity +import org.junit.Before +import org.junit.Rule +import org.junit.Test +import org.junit.runner.RunWith + +import apps.amine.bou.readerforselfoss.settings.SettingsActivity +import apps.amine.bou.readerforselfoss.utils.Config +import org.junit.After + + +@RunWith(AndroidJUnit4::class) +class HomeActivityEspressoTest { + lateinit var context: Context + + @Rule @JvmField + val rule = ActivityTestRule(HomeActivity::class.java, true, false) + + @Before + fun clearData() { + context = InstrumentationRegistry.getInstrumentation().targetContext + + val editor = + context + .getSharedPreferences(Config.settingsName, Context.MODE_PRIVATE) + .edit() + editor.clear() + + editor.putString("url", BuildConfig.LOGIN_URL) + editor.putString("login", BuildConfig.LOGIN_USERNAME) + editor.putString("password", BuildConfig.LOGIN_PASSWORD) + + editor.commit() + + Intents.init() + } + + @Test + fun menuItems() { + + rule.launchActivity(Intent()) + + onView( + withMenu( + id = R.id.action_search, + titleId = R.string.menu_home_search + ) + ).perform(click()) + + onView(withId(R.id.search_bar)).check(matches(isDisplayed())) + + onView(withId(R.id.search_src_text)).perform(typeText("android"), pressKey(KeyEvent.KEYCODE_SEARCH), closeSoftKeyboard()) + + onView(withContentDescription(R.string.abc_toolbar_collapse_description)).perform(click()) + + + onView(withMenu(id = R.id.readAll, titleId = R.string.readAll)).perform(click()) + + openActionBarOverflowOrOptionsMenu(context) + + onView(withMenu(id = R.id.refresh, titleId = R.string.menu_home_refresh)) + .perform(click()) + + openActionBarOverflowOrOptionsMenu(context) + + onView(withText(R.string.action_disconnect)).perform(click()) + + intended(hasComponent(LoginActivity::class.java.name), times(1)) + + onView(isRoot()).perform(pressBack()) + + + } + + @Test + fun drawerTesting() { + + rule.launchActivity(Intent()) + + onView(withId(R.id.material_drawer_layout)).perform(DrawerActions.open()) + + onView(withText(R.string.action_about)).perform(click()) + intended(hasComponent(LibsActivity::class.java.name)) + onView(isRoot()).perform(pressBack()) + intended(hasComponent(HomeActivity::class.java.name)) + + onView(withId(R.id.material_drawer_layout)).perform(DrawerActions.open()) + + onView(withId(R.id.material_drawer_layout)).perform(DrawerActions.open()) + onView(withText(R.string.drawer_action_clear)).perform(click()) + + // bug + //onView(withText(R.string.title_activity_settings)).perform(scrollTo(), click()) + //intended(hasComponent(SettingsActivity::class.java.name)) + + } + + // TODO: test articles opening and actions for cards and lists + + @After + fun releaseIntents() { + Intents.release() + } +} \ No newline at end of file diff --git a/app/src/androidTest/java/apps/amine/bou/readerforselfoss/IntroActivityEspressoTest.kt b/app/src/androidTest/java/apps/amine/bou/readerforselfoss/IntroActivityEspressoTest.kt new file mode 100644 index 0000000..dca631a --- /dev/null +++ b/app/src/androidTest/java/apps/amine/bou/readerforselfoss/IntroActivityEspressoTest.kt @@ -0,0 +1,121 @@ +package apps.amine.bou.readerforselfoss + +import java.util.* + +import android.content.Context +import android.content.Intent +import android.support.test.InstrumentationRegistry.getInstrumentation +import android.support.test.espresso.Espresso.onView +import android.support.test.espresso.action.ViewActions.click +import android.support.test.espresso.assertion.ViewAssertions.matches +import android.support.test.espresso.intent.Intents +import android.support.test.espresso.intent.Intents.intended +import android.support.test.espresso.intent.Intents.times +import android.support.test.espresso.intent.matcher.IntentMatchers.* +import android.support.test.espresso.intent.matcher.UriMatchers.hasHost +import android.support.test.espresso.matcher.ViewMatchers.* +import android.support.test.rule.ActivityTestRule +import android.support.test.runner.AndroidJUnit4 + +import org.hamcrest.Matchers.allOf +import org.hamcrest.Matchers.equalTo +import org.junit.Before +import org.junit.Rule +import org.junit.Test +import org.junit.runner.RunWith + +import apps.amine.bou.readerforselfoss.utils.Config +import org.junit.After + + +@RunWith(AndroidJUnit4::class) +class IntroActivityEspressoTest { + + @Rule @JvmField + val rule = ActivityTestRule(IntroActivity::class.java, true, false) + + @Before + fun clearData() { + val editor = + getInstrumentation().targetContext + .getSharedPreferences(Config.settingsName, Context.MODE_PRIVATE) + .edit() + editor.clear() + editor.commit() + + Intents.init() + } + + @Test + fun nextEachTimes() { + + rule.launchActivity(Intent()) + + onView(withText(R.string.intro_hello_title)).check(matches(isDisplayed())) + onView(withId(R.id.button_next)).perform(click()) + onView(withText(R.string.intro_needs_selfoss_message)).check(matches(isDisplayed())) + onView(withId(R.id.button_next)).perform(click()) + onView(withText(R.string.intro_all_set_message)).check(matches(isDisplayed())) + onView(withId(R.id.button_next)).perform(click()) + + intended(hasComponent(IntroActivity::class.java.name), times(1)) + intended(hasComponent(LoginActivity::class.java.name), times(1)) + + } + + @Test + fun nextBackRandomTimes() { + val max = 5 + val min = 1 + + val random = (Random().nextInt(max + 1 - min)) + min + + rule.launchActivity(Intent()) + + onView(withText(R.string.intro_hello_title)).check(matches(isDisplayed())) + onView(withId(R.id.button_next)).perform(click()) + + repeat(random) {_ -> + onView(withText(R.string.intro_needs_selfoss_message)).check(matches(isDisplayed())) + onView(withId(R.id.button_next)).perform(click()) + onView(withText(R.string.intro_all_set_message)).check(matches(isDisplayed())) + onView(withId(R.id.button_back)).perform(click()) + } + + onView(withId(R.id.button_next)).perform(click()) + onView(withText(R.string.intro_all_set_message)).check(matches(isDisplayed())) + onView(withId(R.id.button_next)).perform(click()) + + intended(hasComponent(IntroActivity::class.java.name), times(1)) + intended(hasComponent(LoginActivity::class.java.name), times(1)) + + } + + @Test + fun clickSelfossUrl() { + rule.launchActivity(Intent()) + + onView(withText(R.string.intro_hello_title)).check(matches(isDisplayed())) + + onView(withId(R.id.button_next)).perform(click()) + + onView(withId(R.id.button_message)).perform(click()) + + intended( + allOf( + hasData( + hasHost( + equalTo("selfoss.aditu.de") + ) + ), + hasAction(Intent.ACTION_VIEW) + ) + ) + + } + + @After + fun releaseIntents() { + Intents.release() + } +} \ No newline at end of file diff --git a/app/src/androidTest/java/apps/amine/bou/readerforselfoss/LoginActivityEspressoTest.kt b/app/src/androidTest/java/apps/amine/bou/readerforselfoss/LoginActivityEspressoTest.kt new file mode 100644 index 0000000..3907cbe --- /dev/null +++ b/app/src/androidTest/java/apps/amine/bou/readerforselfoss/LoginActivityEspressoTest.kt @@ -0,0 +1,164 @@ +package apps.amine.bou.readerforselfoss + +import android.content.Context +import android.content.Intent +import android.support.test.InstrumentationRegistry +import android.support.test.espresso.Espresso.onView +import android.support.test.espresso.Espresso.openActionBarOverflowOrOptionsMenu +import android.support.test.espresso.action.ViewActions.* +import android.support.test.espresso.assertion.ViewAssertions.matches +import android.support.test.espresso.intent.Intents +import android.support.test.espresso.intent.Intents.intended +import android.support.test.espresso.intent.Intents.times +import android.support.test.espresso.intent.matcher.IntentMatchers.hasComponent +import android.support.test.espresso.matcher.ViewMatchers +import android.support.test.espresso.matcher.ViewMatchers.* +import android.support.test.rule.ActivityTestRule +import android.support.test.runner.AndroidJUnit4 + +import com.mikepenz.aboutlibraries.ui.LibsActivity +import org.junit.Before +import org.junit.Rule +import org.junit.Test +import org.junit.runner.RunWith + +import apps.amine.bou.readerforselfoss.utils.Config +import org.junit.After + + +@RunWith(AndroidJUnit4::class) +class LoginActivityEspressoTest { + + @Rule @JvmField + val rule = ActivityTestRule(LoginActivity::class.java, true, false) + + lateinit var context: Context + lateinit var url: String + lateinit var username: String + lateinit var password: String + + @Before + fun setUp() { + context = InstrumentationRegistry.getInstrumentation().targetContext + val editor = + context + .getSharedPreferences(Config.settingsName, Context.MODE_PRIVATE) + .edit() + editor.clear() + editor.commit() + + + url = BuildConfig.LOGIN_URL + username = BuildConfig.LOGIN_USERNAME + password = BuildConfig.LOGIN_PASSWORD + + Intents.init() + } + + @Test + fun menuItems() { + + rule.launchActivity(Intent()) + + openActionBarOverflowOrOptionsMenu(context) + + onView(withText(R.string.action_about)).perform(click()) + + intended(hasComponent(LibsActivity::class.java.name), times(1)) + + onView(isRoot()).perform(pressBack()) + + intended(hasComponent(LoginActivity::class.java.name)) + + } + + + @Test + fun wrongLoginUrl() { + rule.launchActivity(Intent()) + + onView(withId(R.id.login_progress)) + .check(matches(withEffectiveVisibility(ViewMatchers.Visibility.GONE))) + + onView(withId(R.id.url)).perform(click()).perform(typeText("WRONGURL")) + + onView(withId(R.id.email_sign_in_button)).perform(click()) + + onView(withId(R.id.urlLayout)).check(matches(isHintOrErrorEnabled())) + } + + // TODO: Add tests for multiple false urls with dialog + + @Test + fun emptyAuthData() { + + rule.launchActivity(Intent()) + + onView(withId(R.id.url)).perform(click()).perform(typeText(url), closeSoftKeyboard()) + + onView(withId(R.id.withLogin)).perform(click()) + + onView(withId(R.id.email_sign_in_button)).perform(click()) + + onView(withId(R.id.loginLayout)).check(matches(isHintOrErrorEnabled())) + onView(withId(R.id.passwordLayout)).check(matches(isHintOrErrorEnabled())) + + onView(withId(R.id.login)).perform(click()).perform(typeText(username), closeSoftKeyboard()) + + onView(withId(R.id.passwordLayout)).check(matches(isHintOrErrorEnabled())) + + onView(withId(R.id.email_sign_in_button)).perform(click()) + + onView(withId(R.id.passwordLayout)).check( + matches( + isHintOrErrorEnabled()) + ) + + } + + @Test + fun wrongAuthData() { + + rule.launchActivity(Intent()) + + onView(withId(R.id.url)).perform(click()).perform(typeText(url), closeSoftKeyboard()) + + onView(withId(R.id.withLogin)).perform(click()) + + onView(withId(R.id.login)).perform(click()).perform(typeText(username), closeSoftKeyboard()) + + onView(withId(R.id.password)).perform(click()).perform(typeText("WRONGPASS"), closeSoftKeyboard()) + + onView(withId(R.id.email_sign_in_button)).perform(click()) + + onView(withId(R.id.urlLayout)).check(matches(isHintOrErrorEnabled())) + onView(withId(R.id.loginLayout)).check(matches(isHintOrErrorEnabled())) + onView(withId(R.id.passwordLayout)).check(matches(isHintOrErrorEnabled())) + + } + + @Test + fun workingAuth() { + + rule.launchActivity(Intent()) + + onView(withId(R.id.url)).perform(click()).perform(typeText(url), closeSoftKeyboard()) + + onView(withId(R.id.withLogin)).perform(click()) + + onView(withId(R.id.login)).perform(click()).perform(typeText(username), closeSoftKeyboard()) + + onView(withId(R.id.password)).perform(click()).perform(typeText(password), closeSoftKeyboard()) + + onView(withId(R.id.email_sign_in_button)).perform(click()) + + intended(hasComponent(HomeActivity::class.java.name)) + + } + + @After + fun releaseIntents() { + Intents.release() + } + +} \ No newline at end of file diff --git a/app/src/androidTest/java/apps/amine/bou/readerforselfoss/MainActivityEspressoTest.kt b/app/src/androidTest/java/apps/amine/bou/readerforselfoss/MainActivityEspressoTest.kt new file mode 100644 index 0000000..ecd1c96 --- /dev/null +++ b/app/src/androidTest/java/apps/amine/bou/readerforselfoss/MainActivityEspressoTest.kt @@ -0,0 +1,73 @@ +package apps.amine.bou.readerforselfoss + +import android.content.Intent +import android.content.SharedPreferences +import android.preference.PreferenceManager +import android.support.test.InstrumentationRegistry.getInstrumentation +import android.support.test.espresso.intent.Intents +import android.support.test.espresso.intent.Intents.intended +import android.support.test.espresso.intent.Intents.times +import android.support.test.espresso.intent.matcher.IntentMatchers.hasComponent +import android.support.test.rule.ActivityTestRule +import android.support.test.runner.AndroidJUnit4 +import org.junit.After + +import org.junit.Before +import org.junit.Rule +import org.junit.Test +import org.junit.runner.RunWith + + +@RunWith(AndroidJUnit4::class) +class MainActivityEspressoTest { + + lateinit var intent: Intent + lateinit var preferencesEditor: SharedPreferences.Editor + + @Rule @JvmField + val rule = ActivityTestRule(MainActivity::class.java, true, false) + + @Before + fun setUp() { + intent = Intent() + val context = getInstrumentation().targetContext + + // create a SharedPreferences editor + preferencesEditor = PreferenceManager.getDefaultSharedPreferences(context).edit() + + Intents.init() + } + + @Test + fun checkFirstOpenLaunchesIntro() { + preferencesEditor.putBoolean("firstStart", true) + preferencesEditor.commit() + + rule.launchActivity(intent) + + intended(hasComponent(MainActivity::class.java.name)) + intended(hasComponent(IntroActivity::class.java.name)) + intended(hasComponent(LoginActivity::class.java.name), times(0)) + + } + + @Test + fun checkNotFirstOpenLaunchesLogin() { + preferencesEditor.putBoolean("firstStart", false) + preferencesEditor.commit() + + rule.launchActivity(intent) + + intended(hasComponent(MainActivity::class.java.name)) + intended(hasComponent(LoginActivity::class.java.name)) + intended(hasComponent(IntroActivity::class.java.name), times(0)) + + } + + @After + fun releaseIntents() { + Intents.release() + } + + +} \ No newline at end of file diff --git a/app/src/androidTest/java/apps/amine/bou/readerforselfoss/Utils.kt b/app/src/androidTest/java/apps/amine/bou/readerforselfoss/Utils.kt new file mode 100644 index 0000000..0983c75 --- /dev/null +++ b/app/src/androidTest/java/apps/amine/bou/readerforselfoss/Utils.kt @@ -0,0 +1,31 @@ +package apps.amine.bou.readerforselfoss + +import android.view.View +import android.support.design.widget.TextInputLayout +import android.support.test.espresso.matcher.ViewMatchers + +import org.hamcrest.Description +import org.hamcrest.Matcher +import org.hamcrest.TypeSafeMatcher +import org.hamcrest.Matchers + + + +fun isHintOrErrorEnabled(): Matcher = + object : TypeSafeMatcher() { + override fun describeTo(description: Description?) {} + + override fun matchesSafely(item: View?): Boolean { + if (item !is TextInputLayout) { + return false + } + + return item.isHintEnabled || item.isErrorEnabled + } + } + +fun withMenu(id: Int, titleId: Int): Matcher = + Matchers.anyOf( + ViewMatchers.withId(id), + ViewMatchers.withText(titleId) + ) diff --git a/app/src/main/res/layout/activity_login.xml b/app/src/main/res/layout/activity_login.xml index 1a43394..27e0008 100644 --- a/app/src/main/res/layout/activity_login.xml +++ b/app/src/main/res/layout/activity_login.xml @@ -32,6 +32,7 @@ + app:showAsAction="always"/> + android:title="@string/menu_home_refresh" /> + if ("com.android.build.gradle.AppPlugin".equals(plugin.class.name)) { + project.android.dexOptions.preDexLibraries = rootProject.ext.preDexLibs + } else if ("com.android.build.gradle.LibraryPlugin".equals(plugin.class.name)) { + project.android.dexOptions.preDexLibraries = rootProject.ext.preDexLibs + } + } +} \ No newline at end of file diff --git a/circle.yml b/circle.yml index 91405e9..f51f601 100644 --- a/circle.yml +++ b/circle.yml @@ -40,4 +40,17 @@ dependencies: test: override: - - gradlew assemble -P crashlyticsdemoApikey=$FABRIC_API_KEY -P crashlyticsdemoApisecret=$FABRIC_API_SECRET + - (TERM="dumb" ./gradlew assemble --configure-on-demand --no-daemon -P crashlyticsdemoApikey=$FABRIC_API_KEY -P crashlyticsdemoApisecret=$FABRIC_API_SECRET -P appLoginUrl=$LOGIN_URL -P appLoginUsername=$LOGIN_USER_NAME -P appLoginPassword=$LOGIN_PASSWORD -PdisablePreDex -Pandroid.threadPoolSize=1 -Porg.gradle.parallel=false): + timeout: 720 + - emulator -avd circleci-android22 -no-window: + background: true + parallel: true + - circle-android wait-for-boot + - sleep 30 + - adb shell input keyevent 82 + - adb shell input tap 650 300 + - (TERM="dumb" ./gradlew connectedAndroidTest --configure-on-demand --no-daemon --stacktrace -P crashlyticsdemoApikey=$FABRIC_API_KEY -P crashlyticsdemoApisecret=$FABRIC_API_SECRET -P appLoginUrl=$LOGIN_URL -P appLoginUsername=$LOGIN_USER_NAME -P appLoginPassword=$LOGIN_PASSWORD -PdisablePreDex -Pandroid.threadPoolSize=1 -Porg.gradle.parallel=false): + timeout: 720 + - cp -r app/build/outputs $CIRCLE_ARTIFACTS + - cp -r app/build/reports/androidTests $CIRCLE_ARTIFACTS + - cp -r app/build/outputs/androidTest-results/* $CIRCLE_TEST_REPORTS