1#. Google I/O 2018 app — Architecture and Testing https://medium.com/androiddevelopers/google-i-o-2018-app-architecture-and-testing-f546e37fc7eb
2#. The Basics of Android Espresso Testing: Activities & Fragments https://android.jlelse.eu/the-basics-of-android-espresso-testing-activities-fragments-7a8bfbc16dc5
3#. Links
4#. Instrumentation tests
5#. Gradle setup
The Android Testing Support library (ATSL) provides a great framework for testing your Android app. It has a JUnit 4-compatible test runner (AndroidJUnitRunner) and functional UI testing through Espresso and UI Automator. You can run tests for these APIs directly in the Android Studio IDE or from the command line, which makes it very easy to integrate them into your development flow.
Source code: $ git clone https://github.com/googlecodelabs/android-testing
Creating presenter with flavor based injection
@Override
public void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
mListAdapter = new NotesAdapter(new ArrayList<Note>(0), mItemListener);
mActionsListener = new NotesPresenter(Injection.provideNotesRepository(), this);
}
```gradle
flavorDimensions "client", "server"
productFlavors {
prod {
dimension "client"
applicationIdSuffix ".prod"
}
mock {
dimension "client"
applicationIdSuffix ".mock"
}
// Flavor Dimensions allow us to define groups of flavors. Gradle creates every combination between these groups for us. No need to mix and match by hand.
dev {
dimension "server"
}
production {
dimension "server"
}
}
Kotlin default final classes/methods
class VictoryViewModelTest {
// to avoid the check of setting values to LiveData objects on threads that are not the Android main thread
@get:Rule
var instantTaskExecutorRule = InstantTaskExecutorRule()
private val viewStateObserver = mock<Observer<VictoryUiModel>>()
private val mockVictoryRepository: VictoryRepository = mock()
private val viewModel = VictoryViewModel()
@Before
fun setUpTaskDetailViewModel() {
viewModel.viewState.observeForever(viewStateObserver)
viewModel.repository = mockVictoryRepository
}
@Test
fun incrementVictoryCount_CallsRepository() {
stubVictoryRepositoryGetVictoryCount(5) // Arrange
viewModel.incrementVictoryCount() // Act
verify(mockVictoryRepository).getVictoryCount() // Assert
}
@Test
fun incrementVictoryCount_UpdatesCount() {
val previousCount = 5
stubVictoryRepositoryGetVictoryCount(previousCount)
viewModel.incrementVictoryCount()
verify(mockVictoryRepository).setVictoryCount(previousCount + 1)
}
@Test
fun incrementVictoryCount_ReturnsUpdatedCount() {
val previousCount = 5
stubVictoryRepositoryGetVictoryCount(previousCount)
viewModel.incrementVictoryCount()
verify(viewStateObserver)
.onChanged(VictoryUiModel.CountUpdated(previousCount + 1))
}
@Test
fun setVictoryTitle_SavesTitle() {
val title = "New title"
viewModel.setVictoryTitle(title)
verify(mockVictoryRepository).setVictoryTitle(title)
}
@Test
fun setVictoryTitle_ReturnsTitle() {
val title = "New title"
viewModel.setVictoryTitle(title)
verify(viewStateObserver).onChanged(VictoryUiModel.TitleUpdated(title))
}
private fun stubVictoryRepositoryGetVictoryTitleAndCount(titleAndCount: Pair<String, Int>) {
stubVictoryRepositoryGetVictoryTitle(titleAndCount.first)
stubVictoryRepositoryGetVictoryCount(titleAndCount.second)
whenever(mockVictoryRepository.getVictoryTitleAndCount())
.thenReturn(titleAndCount)
}
private fun stubVictoryRepositoryGetVictoryTitle(title: String) {
whenever(mockVictoryRepository.getVictoryTitle())
.thenReturn(title)
}
private fun stubVictoryRepositoryGetVictoryCount(count: Int) {
whenever(mockVictoryRepository.getVictoryCount())
.thenReturn(count)
}
}
testImplementation 'junit:junit:4.12'
androidTestImplementation "android.arch.core:core-testing:$lifecycle_version"
androidTestImplementation "com.nhaarman:mockito-kotlin:1.6.0"
androidTestImplementation "org.mockito:mockito-android:2.23.0"
androidTestImplementation 'com.android.support.test:runner:1.0.2'
androidTestImplementation 'com.android.support.test:rules:1.0.2'
androidTestImplementation 'com.android.support.test.espresso:espresso-core:3.0.2'
class SearchResultsTest {
private lateinit var repository: RecipeRepository
private lateinit var presenter: SearchResultsPresenter
private lateinit var view: SearchResultsPresenter.View
@Before
fun setup() {
repository = mock()
view = mock()
presenter = SearchResultsPresenter(repository)
presenter.attachView(view)
}
@Test
fun `search calls showLoading`() {
presenter.search("eggs")
verify(view).showLoading()
}
@Test
fun `search calls getRecipes`() {
presenter.search("eggs")
verify(repository).getRecipes(eq("eggs"), any())
}
@Test
fun `search withRepositoryHavingRecipes calls ShowRecipes`() {
// setup
val recipe = Recipe("id", "title", "imageUrl", "sourceUrl", false)
val recipes = listOf(recipe)
doAnswer {
val callback: RepositoryCallback<List<Recipe>> = it.getArgument(1)
callback.onSuccess(recipes)
}.whenever(repository).getRecipes(eq("eggs"), any())
// invoke
presenter.search("eggs")
// verify
verify(view).showRecipes(eq(recipes))
}
@Test
fun `addFavorite should updateRecipeStatus`() {
// setup
val recipe = Recipe("id", "title", "imageUrl", "sourceUrl", false)
// invoke
presenter.addFavorite(recipe)
// verify
Assert.assertTrue(recipe.isFavorited)
}
}
class VictoryViewModelTest {
// to avoid the check of setting values to LiveData objects on threads that are not the Android main thread
@get:Rule
var instantTaskExecutorRule = InstantTaskExecutorRule()
private val viewStateObserver = mock<Observer<VictoryUiModel>>()
private val mockVictoryRepository: VictoryRepository = mock()
private val viewModel = VictoryViewModel()
@Before
fun setUpTaskDetailViewModel() {
viewModel.viewState.observeForever(viewStateObserver)
viewModel.repository = mockVictoryRepository
}
@Test
fun initialize_ReturnsTitle() {
val title = "New title"
val count = 5
stubVictoryRepositoryGetVictoryTitleAndCount(Pair(title, count))
viewModel.initialize()
verify(viewStateObserver).onChanged(VictoryUiModel.TitleUpdated(title))
}
@Test
fun initialize_ReturnsCount() {
val title = "New title"
val count = 5
stubVictoryRepositoryGetVictoryTitleAndCount(Pair(title, count))
viewModel.initialize()
verify(viewStateObserver).onChanged(VictoryUiModel.CountUpdated(count))
}
@Test
fun incrementVictoryCount_CallsRepository() {
stubVictoryRepositoryGetVictoryCount(5) // Arrange
viewModel.incrementVictoryCount() // Act
verify(mockVictoryRepository).getVictoryCount() // Assert
}
@Test
fun incrementVictoryCount_UpdatesCount() {
val previousCount = 5
stubVictoryRepositoryGetVictoryCount(previousCount)
viewModel.incrementVictoryCount()
verify(mockVictoryRepository).setVictoryCount(previousCount + 1)
}
@Test
fun incrementVictoryCount_ReturnsUpdatedCount() {
val previousCount = 5
stubVictoryRepositoryGetVictoryCount(previousCount)
viewModel.incrementVictoryCount()
verify(viewStateObserver)
.onChanged(VictoryUiModel.CountUpdated(previousCount + 1))
}
@Test
fun setVictoryTitle_SavesTitle() {
val title = "New title"
viewModel.setVictoryTitle(title)
verify(mockVictoryRepository).setVictoryTitle(title)
}
@Test
fun setVictoryTitle_ReturnsTitle() {
val title = "New title"
viewModel.setVictoryTitle(title)
verify(viewStateObserver).onChanged(VictoryUiModel.TitleUpdated(title))
}
private fun stubVictoryRepositoryGetVictoryTitleAndCount(titleAndCount: Pair<String, Int>) {
stubVictoryRepositoryGetVictoryTitle(titleAndCount.first)
stubVictoryRepositoryGetVictoryCount(titleAndCount.second)
whenever(mockVictoryRepository.getVictoryTitleAndCount())
.thenReturn(titleAndCount)
}
private fun stubVictoryRepositoryGetVictoryTitle(title: String) {
whenever(mockVictoryRepository.getVictoryTitle())
.thenReturn(title)
}
private fun stubVictoryRepositoryGetVictoryCount(count: Int) {
whenever(mockVictoryRepository.getVictoryCount())
.thenReturn(count)
}
}
@LargeTest
class MainActivityTest {
@Rule
@JvmField
var rule = ActivityTestRule(MainActivity::class.java)
@Test
fun tappingOnTitle_opensEditDialog() {
onView(withId(R.id.textVictoryTitle))
.perform(click())
onView(withId(R.id.alertTitle))
.check(matches(isDisplayed()))
onView(withId(android.R.id.button2))
.perform(click())
}
@Test
fun editingTitle_DoesntChangeCount() {
onView(withId(R.id.fab))
.perform(click())
onView(withId(R.id.textVictoryTitle))
.perform(click())
val newTitle = "Made the bed"
onView(instanceOf(EditText::class.java))
.perform(clearText())
.perform(typeText(newTitle))
onView(withText(R.string.dialog_ok))
.perform(click())
onView(allOf(withId(R.id.tvVictoryCount), withText("0")))
.check(doesNotExist())
}
@Test
fun incrementingVictoryCount_updatesCountView() {
val previousCountString = rule.activity.tvVictoryCount.text.toString()
val previousCount =
if (previousCountString.isBlank()) {
0
} else {
previousCountString.toInt()
}
onView(withId(R.id.fab))
.perform(click())
onView(allOf(withId(R.id.tvVictoryCount),
withText((previousCount + 1).toString())))
.check(matches(isDisplayed()))
}
@Test
fun editingDialog_updatesTitle() {
onView(withId(R.id.textVictoryTitle))
.perform(click())
val newTitle = "Made the bed"
onView(instanceOf(EditText::class.java))
.perform(clearText())
.perform(typeText(newTitle))
onView(withText(R.string.dialog_ok))
.perform(click())
onView(allOf(withId(R.id.textVictoryTitle), withText(newTitle)))
.check(matches(isDisplayed()))
}
}