Skip to content

Commit

Permalink
Add basic espresso test (#2)
Browse files Browse the repository at this point in the history
Issues with MotionLayout animations mean the screen never transitions to the ready state so one test is ignored
  • Loading branch information
mattmook authored Jan 26, 2021
1 parent 5398805 commit d9cc3f9
Show file tree
Hide file tree
Showing 12 changed files with 344 additions and 5 deletions.
26 changes: 23 additions & 3 deletions androidApp/build.gradle.kts
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import dagger.hilt.android.plugin.HiltExtension

/*
* Copyright 2021 Matthew Dolan
*
Expand Down Expand Up @@ -40,6 +42,7 @@ dependencies {
implementation("androidx.lifecycle:lifecycle-viewmodel-ktx:${Versions.AndroidX.lifecycle}")
implementation("androidx.navigation:navigation-fragment-ktx:${Versions.AndroidX.navigation}")
implementation("androidx.navigation:navigation-ui-ktx:${Versions.AndroidX.navigation}")
androidTestImplementation("androidx.navigation:navigation-testing:${Versions.AndroidX.navigation}")
implementation("org.orbit-mvi:orbit-viewmodel:${Versions.orbitMvi}")
testImplementation("org.orbit-mvi:orbit-test:${Versions.orbitMvi}")

Expand All @@ -66,12 +69,18 @@ dependencies {
kapt("com.google.dagger:hilt-android-compiler:${Versions.Google.dagger}")
androidTestImplementation("com.google.dagger:hilt-android-testing:${Versions.Google.dagger}")
kaptAndroidTest("com.google.dagger:hilt-android-compiler:${Versions.Google.dagger}")
testImplementation("com.google.dagger:hilt-android-testing:${Versions.Google.dagger}")
kaptTest("com.google.dagger:hilt-android-compiler:${Versions.Google.dagger}")
kaptAndroidTest("androidx.hilt:hilt-compiler:${Versions.AndroidX.hilt}")

// Testing
implementation("androidx.test.espresso:espresso-idling-resource:${Versions.AndroidX.espresso}")
testImplementation("com.nhaarman.mockitokotlin2:mockito-kotlin:${Versions.mockitoKotlin}")

testImplementation("junit:junit:${Versions.junit4}")
androidTestImplementation("androidx.test.espresso:espresso-core:${Versions.AndroidX.espresso}")
androidTestImplementation("androidx.test:runner:${Versions.AndroidX.testRunner}")
androidTestImplementation("androidx.test:rules:${Versions.AndroidX.testRules}")
androidTestImplementation("androidx.test.ext:junit-ktx:${Versions.AndroidX.testExtJunit}")
debugImplementation("androidx.fragment:fragment-testing:${Versions.AndroidX.fragmentTesting}")
androidTestImplementation("com.kaspersky.android-components:kaspresso:${Versions.kaspresso}")

coreLibraryDesugaring("com.android.tools:desugar_jdk_libs:${Versions.desugar}")
}
Expand All @@ -85,6 +94,7 @@ android {
versionCode = 1
versionName = "1.0"
vectorDrawables.useSupportLibrary = true
testInstrumentationRunner = "com.mattdolan.cv.test.HiltTestRunner"
}
buildTypes {
getByName("release") {
Expand All @@ -111,4 +121,14 @@ android {
sourceSets.all {
java.srcDir("src/$name/kotlin")
}

testOptions {
unitTests {
isIncludeAndroidResources = true
}
}
}

configure<HiltExtension> {
enableTransformForLocalTests = true
}
61 changes: 61 additions & 0 deletions androidApp/src/androidTest/java/com/mattdolan/cv/data/MockData.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
/*
* Copyright 2021 Matthew Dolan
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package com.mattdolan.cv.data

import com.mattdolan.cv.domain.Experience
import com.mattdolan.cv.domain.PersonalDetails
import com.mattdolan.cv.domain.Role
import com.mattdolan.cv.domain.RoleDetails
import com.mattdolan.cv.domain.Skill
import kotlin.random.Random

fun Random.Default.nextPersonalDetails() = PersonalDetails(
name = nextString(),
tagline = nextString(),
location = nextString(),
avatarUrl = nextString()
)

fun Random.Default.nextSkill() = Skill(
skill = nextString(),
since = if (nextBoolean()) nextInt(1970, 2100) else null
)

fun Random.Default.nextRole() = Role(
title = nextString(),
team = if (nextBoolean()) nextString() else null,
period = nextString(),
detailUrl = nextString()
)

fun Random.Default.nextExperience() = Experience(
company = nextString(),
logoUrl = nextString(),
industry = nextString(),
location = nextString(),
period = nextString(),
roles = nextList { nextRole() }
)

fun Random.Default.nextRoleDetails() = RoleDetails(
description = nextList { nextString() }
)

fun <T> Random.Default.nextList(generator: () -> T) = (1..nextInt(1, 10)).map { generator() }

private val charPool: List<Char> = ('a'..'z') + ('A'..'Z') + ('0'..'9')
private fun Random.Default.nextString(length: Int = 20) = (1..length).map { charPool[nextInt(0, charPool.size)] }.joinToString("")
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
/*
* Copyright 2021 Matthew Dolan
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package com.mattdolan.cv.data

import com.mattdolan.cv.domain.Experience
import com.mattdolan.cv.domain.PersonalDetails
import com.mattdolan.cv.domain.ProfileRepository
import com.mattdolan.cv.domain.Role
import com.mattdolan.cv.domain.RoleDetails
import com.mattdolan.cv.domain.Skill
import kotlin.random.Random

class MockProfileRepository : ProfileRepository {
var personalDetails: PersonalDetails? = Random.nextPersonalDetails()
override suspend fun personalDetails(): PersonalDetails = personalDetails ?: throw IllegalStateException("Not populated")

var experiences: List<Experience>? = Random.nextList { Random.nextExperience() }
override suspend fun experiences(): List<Experience> = experiences ?: throw IllegalStateException("Not populated")

var skills: List<Skill>? = Random.nextList { Random.nextSkill() }
override suspend fun skills(): List<Skill> = skills ?: throw IllegalStateException("Not populated")

var roleDetails: RoleDetails? = Random.nextRoleDetails()
override suspend fun roleDetails(role: Role): RoleDetails = roleDetails ?: throw IllegalStateException("Not populated")
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
/*
* Copyright 2021 Matthew Dolan
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package com.mattdolan.cv.profile

import androidx.test.core.app.ActivityScenario.launch
import androidx.test.ext.junit.runners.AndroidJUnit4
import com.kaspersky.kaspresso.testcases.api.testcaserule.TestCaseRule
import com.mattdolan.cv.MainActivity
import com.mattdolan.cv.data.MockProfileRepository
import com.mattdolan.cv.di.RepositoryModule
import com.mattdolan.cv.domain.ProfileRepository
import dagger.hilt.android.testing.BindValue
import dagger.hilt.android.testing.HiltAndroidRule
import dagger.hilt.android.testing.HiltAndroidTest
import dagger.hilt.android.testing.UninstallModules
import org.junit.Ignore
import org.junit.Rule
import org.junit.Test
import org.junit.runner.RunWith

// This test does not run through the IDE
// See https://github.com/google/dagger/issues/1956
@RunWith(AndroidJUnit4::class)
@UninstallModules(RepositoryModule::class)
@HiltAndroidTest
class ProfileFragmentTest {

@get:Rule
val hiltRule = HiltAndroidRule(this)

@get:Rule
val testCaseRule = TestCaseRule(javaClass.name)

private val mockProfileRepository = MockProfileRepository()

@BindValue
@JvmField
val profileRepository: ProfileRepository = mockProfileRepository

@Test
fun testStartsInLoadingState() {
// When we launch the activity
launch(MainActivity::class.java).use {

// Then the loading screen is visible
ProfileScreen {

loadingImage {
isVisible()
}
}
}
}

@Test
fun testShowsErrorWhenRepositoryFails() {
// Given the repository throws an exception when loading personal details
mockProfileRepository.personalDetails = null

// When we launch the activity
launch(MainActivity::class.java).use {

// Then the error screen is visible
ProfileScreen {
errorImage {
isVisible()
}
}
}
}

@Ignore("Test does not pass as transition to ready state not occurring")
@Test
fun testShowsProfileDetailsWhenRepositorySucceeds() {
// When we launch the activity
launch(MainActivity::class.java).use {

// Then the ready screen is visible
ProfileScreen {
experiences {
isVisible()
}
}
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
/*
* Copyright 2021 Matthew Dolan
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package com.mattdolan.cv.profile

import android.view.View
import com.agoda.kakao.image.KImageView
import com.agoda.kakao.recycler.KRecyclerItem
import com.agoda.kakao.recycler.KRecyclerView
import com.kaspersky.kaspresso.screens.KScreen
import com.mattdolan.cv.androidApp.R
import org.hamcrest.Matcher

object ProfileScreen : KScreen<ProfileScreen>() {
override val layoutId: Int = R.layout.profile_fragment
override val viewClass: Class<*> = ProfileFragment::class.java

val loadingImage = KImageView { withId(R.id.loading) }

val errorImage = KImageView { withId(R.id.error) }

val experiences: KRecyclerView = KRecyclerView({
withId(R.id.recycler_view)
}, itemTypeBuilder = {
itemType(::RoleItem)
})

class RoleItem(parent: Matcher<View>) : KRecyclerItem<RoleItem>(parent)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
/*
* Copyright 2021 Matthew Dolan
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package com.mattdolan.cv.test

import android.app.Application
import android.content.Context
import androidx.test.runner.AndroidJUnitRunner
import dagger.hilt.android.testing.HiltTestApplication

class HiltTestRunner : AndroidJUnitRunner() {
override fun newApplication(classLoader: ClassLoader?, appName: String?, context: Context?): Application {
println("HiltTestRunner")
return super.newApplication(classLoader, HiltTestApplication::class.java.name, context)
}
}
10 changes: 10 additions & 0 deletions androidApp/src/debug/AndroidManifest.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android">

<application>
<activity
android:name="com.mattdolan.cv.test.HiltTestActivity"
android:exported="false" />
</application>

</manifest>
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
/*
* Copyright (C) 2019 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package com.mattdolan.cv.test

import androidx.appcompat.app.AppCompatActivity
import dagger.hilt.android.AndroidEntryPoint

@AndroidEntryPoint
class HiltTestActivity : AppCompatActivity()
Loading

0 comments on commit d9cc3f9

Please sign in to comment.