- Instrumentation tests added

Took 1 hour 32 minutes
This commit is contained in:
2021-06-13 01:45:10 +01:00
parent 60d0192120
commit f9aac8b755
24 changed files with 323 additions and 76 deletions

Binary file not shown.

View File

@@ -11,7 +11,8 @@ android {
targetSdkVersion 30 targetSdkVersion 30
versionCode 5 versionCode 5
versionName "4.1" versionName "4.1"
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
testInstrumentationRunner "com.appttude.h_mal.easycc.application.TestRunner"
} }
buildTypes { buildTypes {
release { release {
@@ -47,6 +48,7 @@ dependencies {
androidTestImplementation 'androidx.test:runner:1.2.0' androidTestImplementation 'androidx.test:runner:1.2.0'
androidTestImplementation 'androidx.test.espresso:espresso-core:3.2.0' androidTestImplementation 'androidx.test.espresso:espresso-core:3.2.0'
testImplementation "org.jetbrains.kotlin:kotlin-test-junit:$kotlin_version" testImplementation "org.jetbrains.kotlin:kotlin-test-junit:$kotlin_version"
androidTestImplementation 'androidx.test:rules:1.4.0-beta02'
//Retrofit and GSON //Retrofit and GSON
def retrofit_ver = "2.8.1" def retrofit_ver = "2.8.1"

View File

@@ -0,0 +1,31 @@
package com.appttude.h_mal.easycc.application
import android.app.Application
import androidx.test.espresso.idling.CountingIdlingResource
import com.appttude.h_mal.easycc.application.modules.MockRepository
import com.appttude.h_mal.easycc.helper.CurrencyDataHelper
import com.appttude.h_mal.easycc.ui.main.MainViewModelFactory
import org.kodein.di.Kodein
import org.kodein.di.KodeinAware
import org.kodein.di.android.x.androidXModule
import org.kodein.di.generic.bind
import org.kodein.di.generic.instance
import org.kodein.di.generic.provider
import org.kodein.di.generic.singleton
class TestApplication : Application(), KodeinAware {
companion object {
val idlingResources = CountingIdlingResource("Data_loader")
}
// KODEIN DI components declaration
override val kodein by Kodein.lazy {
import(androidXModule(this@TestApplication))
bind() from singleton { MockRepository() }
bind() from singleton { CurrencyDataHelper(instance()) }
bind() from provider { MainViewModelFactory(instance(), instance()) }
}
}

View File

@@ -0,0 +1,22 @@
package com.appttude.h_mal.easycc.application
import android.app.Application
import android.content.Context
import androidx.test.runner.AndroidJUnitRunner
class TestRunner : AndroidJUnitRunner() {
@Throws(
InstantiationException::class,
IllegalAccessException::class,
ClassNotFoundException::class
)
override fun newApplication(
cl: ClassLoader?,
className: String?,
context: Context?
): Application {
return super.newApplication(cl, TestApplication::class.java.name, context)
}
}

View File

@@ -0,0 +1,69 @@
package com.appttude.h_mal.easycc.application.modules
import com.appttude.h_mal.easycc.application.TestApplication.Companion.idlingResources
import com.appttude.h_mal.easycc.data.network.response.CurrencyResponse
import com.appttude.h_mal.easycc.data.network.response.ResponseObject
import com.appttude.h_mal.easycc.data.repository.Repository
import com.appttude.h_mal.easycc.models.CurrencyObject
import kotlinx.coroutines.delay
class MockRepository : Repository {
override suspend fun getDataFromApi(fromCurrency: String, toCurrency: String): ResponseObject {
idlingResources.increment()
delay(500)
return ResponseObject(
results = mapOf(
Pair(
"AUD_GBP", CurrencyObject(
id = "AUD_GBP",
fr = "AUD",
to = "GBP",
value = 0.546181
)
)
)
).also {
idlingResources.decrement()
}
}
override suspend fun getBackupDataFromApi(
fromCurrency: String,
toCurrency: String
): CurrencyResponse {
idlingResources.increment()
delay(500)
return CurrencyResponse(
rates = mapOf(Pair("GBP", 0.54638)),
amount = 1.0,
base = "AUD",
date = "2021-06-11"
).also {
idlingResources.decrement()
}
}
override fun getConversionPair(): Pair<String?, String?> {
return Pair("AUD - Australian Dollar", "GBP - British Pound")
}
override fun setConversionPair(fromCurrency: String, toCurrency: String) {}
override fun getCurrenciesList(): Array<String> =
arrayOf("AUD - Australian Dollar", "GBP - British Pound")
override fun getWidgetConversionPairs(appWidgetId: Int): Pair<String?, String?> {
return Pair(null, null)
}
override fun setWidgetConversionPairs(
fromCurrency: String,
toCurrency: String,
appWidgetId: Int
) {
}
override fun removeWidgetConversionPairs(id: Int) {}
}

View File

@@ -0,0 +1,84 @@
package com.appttude.h_mal.easycc.robots
import android.view.View
import android.view.ViewGroup
import androidx.test.espresso.Espresso
import androidx.test.espresso.action.ViewActions
import androidx.test.espresso.assertion.ViewAssertions
import androidx.test.espresso.matcher.ViewMatchers
import com.appttude.h_mal.easycc.R
import org.hamcrest.Description
import org.hamcrest.Matcher
import org.hamcrest.Matchers
import org.hamcrest.TypeSafeMatcher
fun currencyRobot(func: CurrencyRobot.() -> Unit) = CurrencyRobot()
.apply { func() }
class CurrencyRobot {
fun clickOnTopList() {
Espresso.onView(ViewMatchers.withId(R.id.currency_one)).perform(ViewActions.click())
}
fun clickOnBottomList() {
Espresso.onView(ViewMatchers.withId(R.id.currency_two)).perform(ViewActions.click())
}
fun searchInCurrencyList(search: String) {
Espresso.onView(ViewMatchers.withId(R.id.search_text))
.perform(ViewActions.replaceText(search), ViewActions.closeSoftKeyboard())
}
fun enterValueInTopEditText(text: String) {
Espresso.onView(ViewMatchers.withId(R.id.topInsertValue))
.perform(ViewActions.replaceText(text), ViewActions.closeSoftKeyboard())
}
fun selectItemInCurrencyList() {
Espresso.onData(Matchers.anything())
.inAdapterView(
Matchers.allOf(
ViewMatchers.withId(R.id.list_view),
childAtPosition(
ViewMatchers.withClassName(Matchers.`is`("androidx.cardview.widget.CardView")),
0
)
)
)
.atPosition(0)
.perform(ViewActions.click())
}
fun assertTextInTop(text: String) {
Espresso.onView(
ViewMatchers.withId(R.id.topInsertValue)
).check(ViewAssertions.matches(ViewMatchers.withText(text)))
}
fun assertTextInBottom(text: String) {
Espresso.onView(
ViewMatchers.withId(R.id.bottomInsertValues)
).check(ViewAssertions.matches(ViewMatchers.withText(text)))
}
private fun childAtPosition(
parentMatcher: Matcher<View>, position: Int
): Matcher<View> {
return object : TypeSafeMatcher<View>() {
override fun describeTo(description: Description) {
description.appendText("Child at position $position in parent ")
parentMatcher.describeTo(description)
}
public override fun matchesSafely(view: View): Boolean {
val parent = view.parent
return parent is ViewGroup && parentMatcher.matches(parent)
&& view == parent.getChildAt(position)
}
}
}
}

View File

@@ -0,0 +1,35 @@
@file:Suppress("DEPRECATION")
package com.appttude.h_mal.easycc.ui.main
import androidx.test.filters.LargeTest
import androidx.test.rule.ActivityTestRule
import androidx.test.runner.AndroidJUnit4
import com.appttude.h_mal.easycc.robots.currencyRobot
import org.junit.Rule
import org.junit.Test
import org.junit.runner.RunWith
@LargeTest
@RunWith(AndroidJUnit4::class)
class MainActivityTest {
@Rule
@JvmField
var mActivityTestRule = ActivityTestRule(MainActivity::class.java)
@Test
fun mainActivityTest() {
currencyRobot {
clickOnTopList()
searchInCurrencyList("AUD")
selectItemInCurrencyList()
clickOnBottomList()
searchInCurrencyList("GBP")
selectItemInCurrencyList()
enterValueInTopEditText("1")
assertTextInBottom("0.55")
}
}
}

View File

@@ -30,7 +30,7 @@ class AppClass : Application(), KodeinAware {
bind() from singleton { loggingInterceptor() } bind() from singleton { loggingInterceptor() }
bind() from singleton { QueryInterceptor(instance()) } bind() from singleton { QueryInterceptor(instance()) }
bind() from singleton { CurrencyApi(instance(), instance(), instance()) } bind() from singleton { CurrencyApi(instance(), instance(), instance()) }
bind() from singleton { BackupCurrencyApi(instance(),instance()) } bind() from singleton { BackupCurrencyApi(instance(), instance()) }
bind() from singleton { PreferenceProvider(instance()) } bind() from singleton { PreferenceProvider(instance()) }
bind() from singleton { RepositoryImpl(instance(), instance(), instance()) } bind() from singleton { RepositoryImpl(instance(), instance(), instance()) }
bind() from singleton { CurrencyDataHelper(instance()) } bind() from singleton { CurrencyDataHelper(instance()) }

View File

@@ -4,7 +4,6 @@ import android.content.Context
import android.net.ConnectivityManager import android.net.ConnectivityManager
import android.net.NetworkCapabilities import android.net.NetworkCapabilities
import android.os.Build import android.os.Build
import androidx.annotation.RequiresApi
import okhttp3.Interceptor import okhttp3.Interceptor
import okhttp3.Response import okhttp3.Response
import java.io.IOException import java.io.IOException
@@ -21,7 +20,7 @@ class NetworkConnectionInterceptor(
private val applicationContext = context.applicationContext private val applicationContext = context.applicationContext
override fun intercept(chain: Interceptor.Chain): Response { override fun intercept(chain: Interceptor.Chain): Response {
if (!isInternetAvailable()){ if (!isInternetAvailable()) {
throw IOException("Make sure you have an active data connection") throw IOException("Make sure you have an active data connection")
} }
return chain.proceed(chain.request()) return chain.proceed(chain.request())

View File

@@ -20,12 +20,12 @@ class QueryInterceptor(
val originalHttpUrl: HttpUrl = original.url val originalHttpUrl: HttpUrl = original.url
val url = originalHttpUrl.newBuilder() val url = originalHttpUrl.newBuilder()
.addQueryParameter("apiKey", context.getString(R.string.apiKey)) .addQueryParameter("apiKey", context.getString(R.string.apiKey))
.build() .build()
// Add amended Url back to request // Add amended Url back to request
val requestBuilder: Request.Builder = original.newBuilder() val requestBuilder: Request.Builder = original.newBuilder()
.url(url) .url(url)
val request: Request = requestBuilder.build() val request: Request = requestBuilder.build()
return chain.proceed(request) return chain.proceed(request)

View File

@@ -13,18 +13,18 @@ data class CurrencyResponse(
val amount: Double? = null, val amount: Double? = null,
@field:SerializedName("rates") @field:SerializedName("rates")
var rates : Map<String, Double>? = null, var rates: Map<String, Double>? = null,
@field:SerializedName("base") @field:SerializedName("base")
val base: String? = null val base: String? = null
): CurrencyModelInterface { ) : CurrencyModelInterface {
override fun getCurrencyModel(): CurrencyModel { override fun getCurrencyModel(): CurrencyModel {
return CurrencyModel( return CurrencyModel(
base, base,
rates?.iterator()?.next()?.key, rates?.iterator()?.next()?.key,
rates?.iterator()?.next()?.value ?: 0.0 rates?.iterator()?.next()?.value ?: 0.0
) )
} }
} }

View File

@@ -7,18 +7,17 @@ import com.google.gson.annotations.SerializedName
data class ResponseObject( data class ResponseObject(
@field:SerializedName("query") @field:SerializedName("query")
var query : Any? = null, var query: Any? = null,
@field:SerializedName("results") @field:SerializedName("results")
var results : Map<String, CurrencyObject>? = null var results: Map<String, CurrencyObject>? = null
): CurrencyModelInterface { ) : CurrencyModelInterface {
override fun getCurrencyModel(): CurrencyModel {
val res = results?.iterator()?.next()?.value
return CurrencyModel(
res?.fr,
res?.to,
res?.value ?: 0.0
)
}
override fun getCurrencyModel(): CurrencyModel {
val res = results?.iterator()?.next()?.value
return CurrencyModel(
res?.fr,
res?.to,
res?.value ?: 0.0
)
}
} }

View File

@@ -8,7 +8,6 @@ import android.view.WindowManager
import android.widget.TextView import android.widget.TextView
import androidx.appcompat.app.AppCompatActivity import androidx.appcompat.app.AppCompatActivity
import androidx.databinding.DataBindingUtil import androidx.databinding.DataBindingUtil
import androidx.lifecycle.Observer
import androidx.lifecycle.ViewModelProviders import androidx.lifecycle.ViewModelProviders
import com.appttude.h_mal.easycc.R import com.appttude.h_mal.easycc.R
import com.appttude.h_mal.easycc.databinding.ActivityMainBinding import com.appttude.h_mal.easycc.databinding.ActivityMainBinding

View File

@@ -2,7 +2,7 @@ package com.appttude.h_mal.easycc.ui.main
import androidx.lifecycle.ViewModel import androidx.lifecycle.ViewModel
import androidx.lifecycle.ViewModelProvider import androidx.lifecycle.ViewModelProvider
import com.appttude.h_mal.easycc.data.repository.RepositoryImpl import com.appttude.h_mal.easycc.data.repository.Repository
import com.appttude.h_mal.easycc.helper.CurrencyDataHelper import com.appttude.h_mal.easycc.helper.CurrencyDataHelper
/** /**
@@ -11,7 +11,7 @@ import com.appttude.h_mal.easycc.helper.CurrencyDataHelper
*/ */
@Suppress("UNCHECKED_CAST") @Suppress("UNCHECKED_CAST")
class MainViewModelFactory( class MainViewModelFactory(
private val repository: RepositoryImpl, private val repository: Repository,
private val helper: CurrencyDataHelper private val helper: CurrencyDataHelper
) : ViewModelProvider.NewInstanceFactory() { ) : ViewModelProvider.NewInstanceFactory() {

View File

@@ -8,7 +8,6 @@ import android.view.View
import android.widget.TextView import android.widget.TextView
import androidx.appcompat.app.AppCompatActivity import androidx.appcompat.app.AppCompatActivity
import androidx.databinding.DataBindingUtil import androidx.databinding.DataBindingUtil
import androidx.lifecycle.Observer
import androidx.lifecycle.ViewModelProviders import androidx.lifecycle.ViewModelProviders
import com.appttude.h_mal.easycc.R import com.appttude.h_mal.easycc.R
import com.appttude.h_mal.easycc.databinding.CurrencyAppWidgetConfigureBinding import com.appttude.h_mal.easycc.databinding.CurrencyAppWidgetConfigureBinding

View File

@@ -3,11 +3,11 @@ package com.appttude.h_mal.easycc.utils
import java.lang.Double.valueOf import java.lang.Double.valueOf
import java.text.DecimalFormat import java.text.DecimalFormat
fun transformIntToArray(int: Int): IntArray{ fun transformIntToArray(int: Int): IntArray {
return intArrayOf(int) return intArrayOf(int)
} }
fun String.trimToThree(): String{ fun String.trimToThree(): String {
val size = length val size = length
return when { return when {
size > 3 -> substring(0, 3) size > 3 -> substring(0, 3)
@@ -16,19 +16,19 @@ fun String.trimToThree(): String{
} }
fun convertPairsListToString(s1: String, s2: String): String = fun convertPairsListToString(s1: String, s2: String): String =
"${s1.trimToThree()}_${s2.trimToThree()}" "${s1.trimToThree()}_${s2.trimToThree()}"
fun Double.toTwoDp() = run { fun Double.toTwoDp() = run {
try { try {
val df = DecimalFormat("0.00") val df = DecimalFormat("0.00")
valueOf(df.format(this)) valueOf(df.format(this))
}catch (e: NumberFormatException){ } catch (e: NumberFormatException) {
e.printStackTrace() e.printStackTrace()
this this
} }
} }
fun Double.toTwoDpString(): String{ fun Double.toTwoDpString(): String {
return this.toTwoDp().toBigDecimal().toPlainString() return this.toTwoDp().toBigDecimal().toPlainString()
} }

View File

@@ -5,5 +5,5 @@
android:startColor="#b3001b" android:startColor="#b3001b"
android:endColor="#262626" android:endColor="#262626"
android:type="linear" android:type="linear"
android:angle="45"/> android:angle="45" />
</shape> </shape>

View File

@@ -1,6 +1,7 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android" <shape xmlns:android="http://schemas.android.com/apk/res/android"
android:shape="rectangle" android:padding="10dp"> android:shape="rectangle"
android:padding="10dp">
<solid android:color="@color/colour_two" /> <solid android:color="@color/colour_two" />
<corners <corners
android:bottomRightRadius="22dp" android:bottomRightRadius="22dp"

View File

@@ -4,18 +4,21 @@
android:height="192dp" android:height="192dp"
android:viewportWidth="1080" android:viewportWidth="1080"
android:viewportHeight="1920"> android:viewportHeight="1920">
<path <path android:pathData="M0,0L1080,0L1080,1920L0,1920L0,0Z">
android:pathData="M0,0L1080,0L1080,1920L0,1920L0,0Z"> <aapt:attr name="android:fillColor">
<aapt:attr name="android:fillColor"> <gradient
<gradient android:startY="0"
android:startY="0" android:startX="1080"
android:startX="1080" android:endY="1920"
android:endY="1920" android:endX="0"
android:endX="0" android:type="linear">
android:type="linear"> <item
<item android:offset="0" android:color="#FF315659"/> android:offset="0"
<item android:offset="1" android:color="#FF2978A0"/> android:color="#FF315659" />
</gradient> <item
</aapt:attr> android:offset="1"
</path> android:color="#FF2978A0" />
</gradient>
</aapt:attr>
</path>
</vector> </vector>

View File

@@ -1,5 +1,10 @@
<vector android:height="24dp" android:tint="#FFFFFF" <vector android:height="24dp"
android:viewportHeight="24.0" android:viewportWidth="24.0" android:tint="#FFFFFF"
android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android"> android:viewportHeight="24.0"
<path android:fillColor="#FF000000" android:pathData="M17.65,6.35C16.2,4.9 14.21,4 12,4c-4.42,0 -7.99,3.58 -7.99,8s3.57,8 7.99,8c3.73,0 6.84,-2.55 7.73,-6h-2.08c-0.82,2.33 -3.04,4 -5.65,4 -3.31,0 -6,-2.69 -6,-6s2.69,-6 6,-6c1.66,0 3.14,0.69 4.22,1.78L13,11h7V4l-2.35,2.35z"/> android:viewportWidth="24.0"
android:width="24dp"
xmlns:android="http://schemas.android.com/apk/res/android">
<path
android:fillColor="#FF000000"
android:pathData="M17.65,6.35C16.2,4.9 14.21,4 12,4c-4.42,0 -7.99,3.58 -7.99,8s3.57,8 7.99,8c3.73,0 6.84,-2.55 7.73,-6h-2.08c-0.82,2.33 -3.04,4 -5.65,4 -3.31,0 -6,-2.69 -6,-6s2.69,-6 6,-6c1.66,0 3.14,0.69 4.22,1.78L13,11h7V4l-2.35,2.35z" />
</vector> </vector>

View File

@@ -8,10 +8,10 @@
android:background="#4D000000"> android:background="#4D000000">
<RelativeLayout <RelativeLayout
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content"> android:layout_height="wrap_content">
<TextView <TextView
android:id="@+id/exchangeName" android:id="@+id/exchangeName"
android:layout_width="match_parent" android:layout_width="match_parent"
@@ -24,6 +24,7 @@
android:textColor="#ffffff" android:textColor="#ffffff"
android:text="Rate not set" android:text="Rate not set"
tools:text="AUDGBP" /> tools:text="AUDGBP" />
<ImageView <ImageView
android:id="@+id/refresh_icon" android:id="@+id/refresh_icon"
android:layout_width="18dp" android:layout_width="18dp"
@@ -32,7 +33,7 @@
android:layout_alignParentTop="true" android:layout_alignParentTop="true"
android:layout_alignParentRight="true" android:layout_alignParentRight="true"
android:layout_alignBottom="@id/exchangeName" android:layout_alignBottom="@id/exchangeName"
android:adjustViewBounds="true"/> android:adjustViewBounds="true" />
</RelativeLayout> </RelativeLayout>
<TextView <TextView
@@ -52,13 +53,13 @@
android:textStyle="bold" android:textStyle="bold"
tools:text="0.52646215" /> tools:text="0.52646215" />
<!-- <TextView--> <!-- <TextView-->
<!-- android:id="@+id/lastUpdated"--> <!-- android:id="@+id/lastUpdated"-->
<!-- android:layout_width="match_parent"--> <!-- android:layout_width="match_parent"-->
<!-- android:layout_height="wrap_content"--> <!-- android:layout_height="wrap_content"-->
<!-- android:gravity="center"--> <!-- android:gravity="center"-->
<!-- android:textSize="8sp"--> <!-- android:textSize="8sp"-->
<!-- android:textColor="#ffffff"--> <!-- android:textColor="#ffffff"-->
<!-- android:text=" "--> <!-- android:text=" "-->
<!-- tools:text="Last updated: 03:18 Wed" />--> <!-- tools:text="Last updated: 03:18 Wed" />-->
</LinearLayout> </LinearLayout>

View File

@@ -3,6 +3,7 @@
xmlns:tools="http://schemas.android.com/tools"> xmlns:tools="http://schemas.android.com/tools">
<data> <data>
<variable <variable
name="viewmodel" name="viewmodel"
type="com.appttude.h_mal.easycc.ui.widget.WidgetViewModel" /> type="com.appttude.h_mal.easycc.ui.widget.WidgetViewModel" />
@@ -78,6 +79,5 @@
</RelativeLayout> </RelativeLayout>
</RelativeLayout> </RelativeLayout>
</layout> </layout>

View File

@@ -1,13 +1,11 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<LinearLayout <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="match_parent" android:layout_height="match_parent"
android:backgroundTint="@android:color/transparent" android:backgroundTint="@android:color/transparent"
android:orientation="vertical"> android:orientation="vertical">
<androidx.cardview.widget.CardView <androidx.cardview.widget.CardView xmlns:card_view="http://schemas.android.com/apk/res-auto"
xmlns:card_view="http://schemas.android.com/apk/res-auto"
android:layout_weight="1" android:layout_weight="1"
android:layout_height="0dp" android:layout_height="0dp"
android:layout_width="match_parent" android:layout_width="match_parent"
@@ -15,13 +13,13 @@
card_view:cardBackgroundColor="@color/colour_three" card_view:cardBackgroundColor="@color/colour_three"
android:layout_marginTop="45dp"> android:layout_marginTop="45dp">
<ListView <ListView
android:id="@+id/list_view" android:id="@+id/list_view"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="match_parent"> android:layout_height="match_parent"></ListView>
</ListView>
</androidx.cardview.widget.CardView> </androidx.cardview.widget.CardView>
<androidx.cardview.widget.CardView xmlns:card_view="http://schemas.android.com/apk/res-auto" <androidx.cardview.widget.CardView xmlns:card_view="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"

View File

@@ -8,4 +8,4 @@
android:previewImage="@drawable/example_appwidget_preview" android:previewImage="@drawable/example_appwidget_preview"
android:resizeMode="horizontal|vertical" android:resizeMode="horizontal|vertical"
android:updatePeriodMillis="3600000" android:updatePeriodMillis="3600000"
android:widgetCategory="home_screen|keyguard"/> android:widgetCategory="home_screen|keyguard" />