Skip to content

Commit 8152c20

Browse files
committedOct 12, 2017
Create Espresso UI tests with fake data
1 parent eed5f3a commit 8152c20

File tree

14 files changed

+308
-72
lines changed

14 files changed

+308
-72
lines changed
 
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
package com.example.tamaskozmer.kotlinrxexample.fakes.di.components
2+
3+
import com.example.tamaskozmer.kotlinrxexample.di.components.ApplicationComponent
4+
import com.example.tamaskozmer.kotlinrxexample.fakes.di.modules.FakeApplicationModule
5+
import dagger.Component
6+
import javax.inject.Singleton
7+
8+
/**
9+
* Created by Tamas_Kozmer on 8/8/2017.
10+
*/
11+
@Singleton
12+
@Component(modules = arrayOf(FakeApplicationModule::class))
13+
interface FakeApplicationComponent : ApplicationComponent
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
package com.example.tamaskozmer.kotlinrxexample.fakes.di.modules
2+
3+
import com.example.tamaskozmer.kotlinrxexample.fakes.model.repositories.FakeUserRepository
4+
import com.example.tamaskozmer.kotlinrxexample.model.repositories.UserRepository
5+
import com.example.tamaskozmer.kotlinrxexample.util.AppSchedulerProvider
6+
import com.example.tamaskozmer.kotlinrxexample.util.SchedulerProvider
7+
import dagger.Module
8+
import dagger.Provides
9+
import javax.inject.Singleton
10+
11+
/**
12+
* Created by Tamas_Kozmer on 8/8/2017.
13+
*/
14+
@Module
15+
class FakeApplicationModule {
16+
17+
@Provides
18+
@Singleton
19+
fun provideUserRepository() : UserRepository {
20+
return FakeUserRepository()
21+
}
22+
23+
@Provides
24+
@Singleton
25+
fun provideSchedulerProvider() : SchedulerProvider = AppSchedulerProvider()
26+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
package com.example.tamaskozmer.kotlinrxexample.fakes.model.repositories
2+
3+
import com.example.tamaskozmer.kotlinrxexample.model.entities.User
4+
import com.example.tamaskozmer.kotlinrxexample.model.entities.UserListModel
5+
import com.example.tamaskozmer.kotlinrxexample.model.repositories.UserRepository
6+
import io.reactivex.Single
7+
import io.reactivex.SingleEmitter
8+
9+
/**
10+
* Created by Tamas_Kozmer on 8/8/2017.
11+
*/
12+
class FakeUserRepository : UserRepository {
13+
14+
override fun getUsers(page: Int, forced: Boolean): Single<UserListModel> {
15+
val users = (1..10L).map {
16+
val number = (page - 1) * 10 + it
17+
User(it, "User $number", number * 100, "")
18+
}
19+
20+
return Single.create<UserListModel> { emitter: SingleEmitter<UserListModel> ->
21+
val userListModel = UserListModel(users)
22+
emitter.onSuccess(userListModel)
23+
}
24+
}
25+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,91 @@
1+
package com.example.tamaskozmer.kotlinrxexample.presentation.view.activities
2+
3+
import android.content.Intent
4+
import android.support.test.InstrumentationRegistry
5+
import android.support.test.espresso.Espresso
6+
import android.support.test.espresso.action.ViewActions
7+
import android.support.test.espresso.assertion.ViewAssertions
8+
import android.support.test.espresso.contrib.RecyclerViewActions
9+
import android.support.test.espresso.matcher.ViewMatchers
10+
import android.support.test.rule.ActivityTestRule
11+
import android.support.test.runner.AndroidJUnit4
12+
import android.support.v7.widget.RecyclerView
13+
import com.example.tamaskozmer.kotlinrxexample.CustomApplication
14+
import com.example.tamaskozmer.kotlinrxexample.R
15+
import com.example.tamaskozmer.kotlinrxexample.fakes.di.components.DaggerFakeApplicationComponent
16+
import com.example.tamaskozmer.kotlinrxexample.fakes.di.modules.FakeApplicationModule
17+
import com.example.tamaskozmer.kotlinrxexample.testutil.RecyclerViewMatcher
18+
import com.example.tamaskozmer.kotlinrxexample.view.activities.MainActivity
19+
import org.hamcrest.Matchers
20+
import org.junit.Before
21+
import org.junit.Rule
22+
import org.junit.Test
23+
import org.junit.runner.RunWith
24+
25+
26+
/**
27+
* Created by Tamas_Kozmer on 8/8/2017.
28+
*/
29+
@RunWith(AndroidJUnit4::class)
30+
class MainActivityTest {
31+
32+
@Rule @JvmField
33+
var activityRule = ActivityTestRule(MainActivity::class.java, true, false)
34+
35+
@Before
36+
fun setUp() {
37+
val instrumentation = InstrumentationRegistry.getInstrumentation()
38+
val app = instrumentation.targetContext.applicationContext as CustomApplication
39+
40+
val testComponent = DaggerFakeApplicationComponent.builder()
41+
.fakeApplicationModule(FakeApplicationModule())
42+
.build()
43+
app.component = testComponent
44+
45+
activityRule.launchActivity(Intent())
46+
}
47+
48+
@Test
49+
fun testRecyclerViewShowingCorrectItems() {
50+
checkNameOnPosition(0, "User 1")
51+
checkNameOnPosition(2, "User 3")
52+
}
53+
54+
@Test
55+
fun testRecyclerViewShowingCorrectItemsAfterScroll() {
56+
Espresso.onView(ViewMatchers.withId(R.id.recyclerView))
57+
.perform(RecyclerViewActions.scrollToPosition<RecyclerView.ViewHolder>(8))
58+
59+
checkNameOnPosition(8, "User 9")
60+
}
61+
62+
@Test
63+
fun testRecyclerViewShowingCorrectItemsAfterPagination() {
64+
// Trigger pagination
65+
Espresso.onView(ViewMatchers.withId(R.id.recyclerView))
66+
.perform(RecyclerViewActions.scrollToPosition<RecyclerView.ViewHolder>(9))
67+
68+
// Scroll to a position on the next page
69+
Espresso.onView(ViewMatchers.withId(R.id.recyclerView))
70+
.perform(RecyclerViewActions.scrollToPosition<RecyclerView.ViewHolder>(15))
71+
72+
// Check if view is showing the correct text
73+
checkNameOnPosition(15, "User 16")
74+
}
75+
76+
private fun checkNameOnPosition(position: Int, expectedName: String) {
77+
Espresso.onView(RecyclerViewMatcher(R.id.recyclerView).atPositionOnView(position, R.id.name))
78+
.check(ViewAssertions.matches(ViewMatchers.withText(expectedName)))
79+
}
80+
81+
@Test
82+
fun testOpenDetailsOnItemClick() {
83+
Espresso.onView(ViewMatchers.withId(R.id.recyclerView))
84+
.perform(RecyclerViewActions.actionOnItemAtPosition<RecyclerView.ViewHolder>(0, ViewActions.click()))
85+
86+
val expectedText = "User 1: 100 pts"
87+
88+
Espresso.onView(Matchers.allOf(ViewMatchers.withId(android.support.design.R.id.snackbar_text), ViewMatchers.withText(expectedText)))
89+
.check(ViewAssertions.matches(ViewMatchers.isDisplayed()));
90+
}
91+
}
Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
package com.example.tamaskozmer.kotlinrxexample.testutil;
2+
3+
import android.content.res.Resources;
4+
import android.support.v7.widget.RecyclerView;
5+
import android.view.View;
6+
7+
import org.hamcrest.Description;
8+
import org.hamcrest.Matcher;
9+
import org.hamcrest.TypeSafeMatcher;
10+
11+
/**
12+
* Helper class to match RecyclerView items by Danny Roa
13+
* https://github.com/dannyroa/espresso-samples/blob/master/RecyclerView/app/src/androidTest/java/com/dannyroa/espresso_samples/recyclerview/RecyclerViewMatcher.java
14+
* Created by dannyroa on 5/10/15.
15+
*/
16+
public class RecyclerViewMatcher {
17+
private final int recyclerViewId;
18+
19+
public RecyclerViewMatcher(int recyclerViewId) {
20+
this.recyclerViewId = recyclerViewId;
21+
}
22+
23+
public Matcher<View> atPosition(final int position) {
24+
return atPositionOnView(position, -1);
25+
}
26+
27+
public Matcher<View> atPositionOnView(final int position, final int targetViewId) {
28+
29+
return new TypeSafeMatcher<View>() {
30+
Resources resources = null;
31+
View childView;
32+
33+
public void describeTo(Description description) {
34+
String idDescription = Integer.toString(recyclerViewId);
35+
if (this.resources != null) {
36+
try {
37+
idDescription = this.resources.getResourceName(recyclerViewId);
38+
} catch (Resources.NotFoundException var4) {
39+
idDescription = String.format("%s (resource name not found)", recyclerViewId);
40+
}
41+
}
42+
43+
description.appendText("with id: " + idDescription);
44+
}
45+
46+
public boolean matchesSafely(View view) {
47+
48+
this.resources = view.getResources();
49+
50+
if (childView == null) {
51+
RecyclerView recyclerView =
52+
view.getRootView().findViewById(recyclerViewId);
53+
if (recyclerView != null && recyclerView.getId() == recyclerViewId) {
54+
childView = recyclerView.findViewHolderForAdapterPosition(position).itemView;
55+
} else {
56+
return false;
57+
}
58+
}
59+
60+
if (targetViewId == -1) {
61+
return view == childView;
62+
} else {
63+
View targetView = childView.findViewById(targetViewId);
64+
return view == targetView;
65+
}
66+
}
67+
};
68+
}
69+
}

‎app/src/main/java/com/example/tamaskozmer/kotlinrxexample/di/components/ApplicationComponent.kt

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ package com.example.tamaskozmer.kotlinrxexample.di.components
22

33
import com.example.tamaskozmer.kotlinrxexample.CustomApplication
44
import com.example.tamaskozmer.kotlinrxexample.di.modules.ApplicationModule
5-
import com.example.tamaskozmer.kotlinrxexample.di.modules.UserListFragmentModule
5+
import com.example.tamaskozmer.kotlinrxexample.di.modules.MainActivityModule
66
import dagger.Component
77
import javax.inject.Singleton
88

@@ -13,5 +13,5 @@ import javax.inject.Singleton
1313
@Component(modules = arrayOf(ApplicationModule::class))
1414
interface ApplicationComponent {
1515
fun inject(application: CustomApplication)
16-
fun plus(userListFragmentModule: UserListFragmentModule) : UserListFragmentComponent
16+
fun plus(mainActivityModule: MainActivityModule) : MainActivityComponent
1717
}
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,13 @@
11
package com.example.tamaskozmer.kotlinrxexample.di.components
22

3-
import com.example.tamaskozmer.kotlinrxexample.di.modules.UserListFragmentModule
3+
import com.example.tamaskozmer.kotlinrxexample.di.modules.MainActivityModule
44
import com.example.tamaskozmer.kotlinrxexample.presentation.presenters.UserListPresenter
55
import dagger.Subcomponent
66

77
/**
88
* Created by Tamas_Kozmer on 7/4/2017.
99
*/
10-
@Subcomponent(modules = arrayOf(UserListFragmentModule::class))
11-
interface UserListFragmentComponent {
10+
@Subcomponent(modules = arrayOf(MainActivityModule::class))
11+
interface MainActivityComponent {
1212
fun presenter() : UserListPresenter
1313
}

‎app/src/main/java/com/example/tamaskozmer/kotlinrxexample/di/modules/ApplicationModule.kt

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import android.arch.persistence.room.Room
44
import android.content.Context
55
import com.example.tamaskozmer.kotlinrxexample.CustomApplication
66
import com.example.tamaskozmer.kotlinrxexample.model.persistence.AppDatabase
7+
import com.example.tamaskozmer.kotlinrxexample.model.repositories.DefaultUserRepository
78
import com.example.tamaskozmer.kotlinrxexample.model.repositories.UserRepository
89
import com.example.tamaskozmer.kotlinrxexample.model.services.UserService
910
import com.example.tamaskozmer.kotlinrxexample.util.*
@@ -39,7 +40,7 @@ class ApplicationModule(val application: CustomApplication) {
3940
@Singleton
4041
fun provideUserRepository(retrofit: Retrofit, database: AppDatabase, connectionHelper: ConnectionHelper,
4142
preferencesHelper: PreferencesHelper, calendarWrapper: CalendarWrapper): UserRepository {
42-
return UserRepository(
43+
return DefaultUserRepository(
4344
retrofit.create(UserService::class.java),
4445
database.userDao(),
4546
connectionHelper,

‎app/src/main/java/com/example/tamaskozmer/kotlinrxexample/di/modules/UserListFragmentModule.kt renamed to ‎app/src/main/java/com/example/tamaskozmer/kotlinrxexample/di/modules/MainActivityModule.kt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ import dagger.Provides
1111
* Created by Tamas_Kozmer on 7/4/2017.
1212
*/
1313
@Module
14-
class UserListFragmentModule() {
14+
class MainActivityModule() {
1515
@Provides
1616
fun provideGetUsers(userRepository: UserRepository) = GetUsers(userRepository)
1717

Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
package com.example.tamaskozmer.kotlinrxexample.model.repositories
2+
3+
import com.example.tamaskozmer.kotlinrxexample.model.entities.UserListModel
4+
import com.example.tamaskozmer.kotlinrxexample.model.persistence.daos.UserDao
5+
import com.example.tamaskozmer.kotlinrxexample.model.services.UserService
6+
import com.example.tamaskozmer.kotlinrxexample.util.CalendarWrapper
7+
import com.example.tamaskozmer.kotlinrxexample.util.ConnectionHelper
8+
import com.example.tamaskozmer.kotlinrxexample.util.Constants
9+
import com.example.tamaskozmer.kotlinrxexample.util.PreferencesHelper
10+
import io.reactivex.Single
11+
import io.reactivex.SingleEmitter
12+
13+
/**
14+
* Created by Tamas_Kozmer on 7/4/2017.
15+
*/
16+
class DefaultUserRepository(
17+
private val userService: UserService,
18+
private val userDao: UserDao,
19+
private val connectionHelper: ConnectionHelper,
20+
private val preferencesHelper: PreferencesHelper,
21+
private val calendarWrapper: CalendarWrapper) : UserRepository {
22+
23+
private val LAST_UPDATE_KEY = "last_update_page_"
24+
25+
override fun getUsers(page: Int, forced: Boolean): Single<UserListModel> {
26+
return Single.create<UserListModel> { emitter: SingleEmitter<UserListModel> ->
27+
if (shouldUpdate(page, forced)) {
28+
loadUsersFromNetwork(page, emitter)
29+
} else {
30+
loadOfflineUsers(page, emitter)
31+
}
32+
}
33+
}
34+
35+
private fun shouldUpdate(page: Int, forced: Boolean) = when {
36+
forced -> true
37+
!connectionHelper.isOnline() -> false
38+
else -> {
39+
val lastUpdate = preferencesHelper.loadLong(LAST_UPDATE_KEY + page)
40+
val currentTime = calendarWrapper.getCurrentTimeInMillis()
41+
lastUpdate + Constants.REFRESH_LIMIT < currentTime
42+
}
43+
}
44+
45+
private fun loadUsersFromNetwork(page: Int, emitter: SingleEmitter<UserListModel>) {
46+
try {
47+
val users = userService.getUsers(page).execute().body()
48+
if (users != null) {
49+
userDao.insertAll(users.items)
50+
val currentTime = calendarWrapper.getCurrentTimeInMillis()
51+
preferencesHelper.saveLong(LAST_UPDATE_KEY + page, currentTime)
52+
emitter.onSuccess(users)
53+
} else {
54+
emitter.onError(Exception("No data received"))
55+
}
56+
} catch (exception: Exception) {
57+
emitter.onError(exception)
58+
}
59+
}
60+
61+
private fun loadOfflineUsers(page: Int, emitter: SingleEmitter<UserListModel>) {
62+
val users = userDao.getUsers(page)
63+
if (!users.isEmpty()) {
64+
emitter.onSuccess(UserListModel(users))
65+
} else {
66+
emitter.onError(Exception("Device is offline"))
67+
}
68+
}
69+
}
Lines changed: 3 additions & 61 deletions
Original file line numberDiff line numberDiff line change
@@ -1,69 +1,11 @@
11
package com.example.tamaskozmer.kotlinrxexample.model.repositories
22

33
import com.example.tamaskozmer.kotlinrxexample.model.entities.UserListModel
4-
import com.example.tamaskozmer.kotlinrxexample.model.persistence.daos.UserDao
5-
import com.example.tamaskozmer.kotlinrxexample.model.services.UserService
6-
import com.example.tamaskozmer.kotlinrxexample.util.CalendarWrapper
7-
import com.example.tamaskozmer.kotlinrxexample.util.ConnectionHelper
8-
import com.example.tamaskozmer.kotlinrxexample.util.Constants
9-
import com.example.tamaskozmer.kotlinrxexample.util.PreferencesHelper
104
import io.reactivex.Single
11-
import io.reactivex.SingleEmitter
125

136
/**
14-
* Created by Tamas_Kozmer on 7/4/2017.
7+
* Created by Tamas_Kozmer on 8/8/2017.
158
*/
16-
class UserRepository(
17-
private val userService: UserService,
18-
private val userDao: UserDao,
19-
private val connectionHelper: ConnectionHelper,
20-
private val preferencesHelper: PreferencesHelper,
21-
private val calendarWrapper: CalendarWrapper) {
22-
23-
private val LAST_UPDATE_KEY = "last_update_page_"
24-
25-
fun getUsers(page: Int, forced: Boolean): Single<UserListModel> {
26-
return Single.create<UserListModel> { emitter: SingleEmitter<UserListModel> ->
27-
if (shouldUpdate(page, forced)) {
28-
loadUsersFromNetwork(page, emitter)
29-
} else {
30-
loadOfflineUsers(page, emitter)
31-
}
32-
}
33-
}
34-
35-
private fun shouldUpdate(page: Int, forced: Boolean) = when {
36-
forced -> true
37-
!connectionHelper.isOnline() -> false
38-
else -> {
39-
val lastUpdate = preferencesHelper.loadLong(LAST_UPDATE_KEY + page)
40-
val currentTime = calendarWrapper.getCurrentTimeInMillis()
41-
lastUpdate + Constants.REFRESH_LIMIT < currentTime
42-
}
43-
}
44-
45-
private fun loadUsersFromNetwork(page: Int, emitter: SingleEmitter<UserListModel>) {
46-
try {
47-
val users = userService.getUsers(page).execute().body()
48-
if (users != null) {
49-
userDao.insertAll(users.items)
50-
val currentTime = calendarWrapper.getCurrentTimeInMillis()
51-
preferencesHelper.saveLong(LAST_UPDATE_KEY + page, currentTime)
52-
emitter.onSuccess(users)
53-
} else {
54-
emitter.onError(Exception("No data received"))
55-
}
56-
} catch (exception: Exception) {
57-
emitter.onError(exception)
58-
}
59-
}
60-
61-
private fun loadOfflineUsers(page: Int, emitter: SingleEmitter<UserListModel>) {
62-
val users = userDao.getUsers(page)
63-
if (!users.isEmpty()) {
64-
emitter.onSuccess(UserListModel(users))
65-
} else {
66-
emitter.onError(Exception("Device is offline"))
67-
}
68-
}
9+
interface UserRepository {
10+
fun getUsers(page: Int = 1, forced: Boolean = false): Single<UserListModel>
6911
}

‎app/src/main/java/com/example/tamaskozmer/kotlinrxexample/presentation/view/activities/MainActivity.kt

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ import android.support.v7.widget.RecyclerView
88
import android.view.View
99
import android.widget.Toast
1010
import com.example.tamaskozmer.kotlinrxexample.R
11-
import com.example.tamaskozmer.kotlinrxexample.di.modules.UserListFragmentModule
11+
import com.example.tamaskozmer.kotlinrxexample.di.modules.MainActivityModule
1212
import com.example.tamaskozmer.kotlinrxexample.presentation.presenters.UserListPresenter
1313
import com.example.tamaskozmer.kotlinrxexample.presentation.view.UserListView
1414
import com.example.tamaskozmer.kotlinrxexample.presentation.view.viewmodels.UserViewModel
@@ -19,7 +19,7 @@ import kotlinx.android.synthetic.main.activity_main.*
1919
class MainActivity : AppCompatActivity(), UserListView {
2020

2121
private val presenter: UserListPresenter by lazy { component.presenter() }
22-
private val component by lazy { customApplication.component.plus(UserListFragmentModule()) }
22+
private val component by lazy { customApplication.component.plus(MainActivityModule()) }
2323
private val adapter by lazy {
2424
val userList = mutableListOf<UserViewModel>()
2525
UserListAdapter(userList) {

‎app/src/test/java/com/example/tamaskozmer/kotlinrxexample/model/repositories/UserRepositoryTest.kt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -47,7 +47,7 @@ class UserRepositoryTest {
4747
@Before
4848
fun setup() {
4949
MockitoAnnotations.initMocks(this)
50-
userRepository = UserRepository(mockUserService, mockUserDao, mockConnectionHelper, mockPreferencesHelper, mockCalendarWrapper)
50+
userRepository = DefaultUserRepository(mockUserService, mockUserDao, mockConnectionHelper, mockPreferencesHelper, mockCalendarWrapper)
5151
}
5252

5353
@Test

‎build.gradle

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
// Top-level build file where you can add configuration options common to all sub-projects/modules.
22

33
buildscript {
4-
ext.kotlin_version = '1.1.3-2'
4+
ext.kotlin_version = '1.1.4-3'
55
repositories {
66
google()
77
jcenter()

0 commit comments

Comments
 (0)
Please sign in to comment.