Skip to content

Commit

Permalink
Merge pull request #287 from wealthfront/more-sample-tests
Browse files Browse the repository at this point in the history
Basic unit tests for all sample-migration screens
  • Loading branch information
cmathew committed Sep 27, 2023
2 parents e670df6 + 701d685 commit 9d57a03
Show file tree
Hide file tree
Showing 18 changed files with 252 additions and 23 deletions.
1 change: 1 addition & 0 deletions magellan-sample-migration/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,7 @@ dependencies {
testImplementation Libs.truth
testImplementation Libs.mockito
testImplementation Libs.robolectric
testImplementation Libs.truth

androidTestImplementation Libs.extJunit
androidTestImplementation Libs.rxjava2
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

import com.wealthfront.magellan.sample.migration.tide.DogBreedsStep;
import com.wealthfront.magellan.sample.migration.tide.DogDetailsScreen;
import com.wealthfront.magellan.sample.migration.tide.DogListStep;
import com.wealthfront.magellan.sample.migration.tide.HelpScreen;

import javax.inject.Singleton;
Expand All @@ -14,6 +15,8 @@ public interface AppComponent {

void inject(MainActivity activity);

void inject(DogListStep step);

void inject(DogDetailsScreen screen);

void inject(DogBreedsStep step);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

import com.wealthfront.magellan.navigation.NavigationTraverser;
import com.wealthfront.magellan.sample.migration.api.DogApi;
import com.wealthfront.magellan.sample.migration.toolbar.ToolbarHelper;

import javax.inject.Singleton;

Expand All @@ -19,6 +20,12 @@ final class AppModule {

private static final String DOG_BASE_URL = "https://dog.ceo/api/";

@Provides
@Singleton
ToolbarHelper provideToolbarHelper() {
return new ToolbarHelper();
}

@Provides
@Singleton
Expedition provideExpedition() {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,20 +20,21 @@ class Expedition @Inject constructor() : LegacyJourney<ExpeditionBinding>(
) {

@set:Inject var navListener: LoggingNavigableListener by attachLateinitFieldToLifecycle()
@Inject lateinit var toolbarHelper: ToolbarHelper
private val lifecycleMetricsListener by attachFieldToLifecycle(LifecycleMetricsListener())

override fun onCreate(context: Context) {
app(context).injector().inject(this)
attachToLifecycle(ToolbarHelper)
attachToLifecycle(toolbarHelper)
}

override fun onShow(context: Context, binding: ExpeditionBinding) {
ToolbarHelper.init(viewBinding!!.menu, navigator)
toolbarHelper.init(viewBinding!!.menu, navigator)
navigator.showNow(DogListStep(::goToDetailsScreen))
}

override fun onDestroy(context: Context) {
removeFromLifecycle(ToolbarHelper)
removeFromLifecycle(toolbarHelper)
}

private fun goToDetailsScreen(name: String) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,8 @@ import android.widget.Toast.LENGTH_SHORT
import com.wealthfront.magellan.core.Step
import com.wealthfront.magellan.coroutines.ShownLifecycleScope
import com.wealthfront.magellan.lifecycle.attachFieldToLifecycle
import com.wealthfront.magellan.sample.migration.AppComponentContainer
import com.wealthfront.magellan.sample.migration.R
import com.wealthfront.magellan.sample.migration.SampleApplication.Companion.app
import com.wealthfront.magellan.sample.migration.api.DogApi
import com.wealthfront.magellan.sample.migration.databinding.DogBreedBinding
import kotlinx.coroutines.launch
Expand All @@ -24,7 +24,7 @@ class DogBreedsStep : Step<DogBreedBinding>(DogBreedBinding::inflate) {
private val scope by attachFieldToLifecycle(ShownLifecycleScope())

override fun onCreate(context: Context) {
app(context).injector().inject(this)
(context.applicationContext as AppComponentContainer).injector().inject(this)
}

override fun onShow(context: Context, binding: DogBreedBinding) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,33 +3,37 @@ package com.wealthfront.magellan.sample.migration.tide
import android.content.Context
import android.view.View
import android.widget.Toast
import com.wealthfront.magellan.OpenForMocking
import com.wealthfront.magellan.Screen
import com.wealthfront.magellan.lifecycle.attachFieldToLifecycle
import com.wealthfront.magellan.rx2.RxDisposer
import com.wealthfront.magellan.sample.migration.AppComponentContainer
import com.wealthfront.magellan.sample.migration.R
import com.wealthfront.magellan.sample.migration.SampleApplication.Companion.app
import com.wealthfront.magellan.sample.migration.api.DogApi
import com.wealthfront.magellan.sample.migration.toolbar.ToolbarHelper
import com.wealthfront.magellan.transitions.CircularRevealTransition
import io.reactivex.android.schedulers.AndroidSchedulers.mainThread
import javax.inject.Inject

@OpenForMocking
class DogDetailsScreen(private val breed: String) : Screen<DogDetailsView>() {

@Inject lateinit var api: DogApi
@Inject lateinit var toolbarHelper: ToolbarHelper
private val rxUnsubscriber by attachFieldToLifecycle(RxDisposer())

override fun createView(context: Context): DogDetailsView {
app(context).injector().inject(this)
(context.applicationContext as AppComponentContainer).injector().inject(this)
return DogDetailsView(context)
}

override fun onShow(context: Context) {
ToolbarHelper.setTitle("Dog Breed Info")
ToolbarHelper.setMenuIcon(R.drawable.clock_white) {
toolbarHelper.setTitle("Dog Breed Info")
toolbarHelper.setMenuIcon(R.drawable.clock_white) {
Toast.makeText(activity, "Menu - Notifications clicked", Toast.LENGTH_SHORT).show()
}
ToolbarHelper.setMenuColor(R.color.water)
toolbarHelper.setMenuColor(R.color.water)

rxUnsubscriber.autoDispose(
api.getRandomImageForBreed(breed)
.observeOn(mainThread())
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,10 @@ import android.content.Context
import android.view.LayoutInflater
import com.bumptech.glide.Glide
import com.wealthfront.magellan.BaseScreenView
import com.wealthfront.magellan.OpenForMocking
import com.wealthfront.magellan.sample.migration.databinding.DogDetailsBinding

@OpenForMocking
class DogDetailsView(context: Context) : BaseScreenView<DogDetailsScreen>(context) {

private val viewBinding = DogDetailsBinding.inflate(LayoutInflater.from(context), this, true)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,15 +6,23 @@ import android.view.ViewGroup
import android.widget.ArrayAdapter
import android.widget.TextView
import com.wealthfront.magellan.core.Step
import com.wealthfront.magellan.sample.migration.AppComponentContainer
import com.wealthfront.magellan.sample.migration.R
import com.wealthfront.magellan.sample.migration.databinding.DashboardBinding
import com.wealthfront.magellan.sample.migration.toolbar.ToolbarHelper
import java.util.Locale
import javax.inject.Inject

class DogListStep(private val goToDogDetails: (name: String) -> Unit) : Step<DashboardBinding>(DashboardBinding::inflate) {

@Inject lateinit var toolbarHelper: ToolbarHelper

override fun onCreate(context: Context) {
(context.applicationContext as AppComponentContainer).injector().inject(this)
}

override fun onShow(context: Context, binding: DashboardBinding) {
ToolbarHelper.setTitle(context.getText(R.string.app_name))
toolbarHelper.setTitle(context.getText(R.string.app_name))
binding.dogItems.adapter = DogListAdapter(context)
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package com.wealthfront.magellan.sample.migration.toolbar

import android.content.Context
import com.wealthfront.magellan.Navigator
import com.wealthfront.magellan.OpenForMocking
import com.wealthfront.magellan.lifecycle.LifecycleAwareComponent
import com.wealthfront.magellan.navigation.NavigationListener
import com.wealthfront.magellan.navigation.NavigationPropagator
Expand All @@ -12,7 +13,8 @@ import com.wealthfront.magellan.navigation.NavigationPropagator
* Ideally, this dependency is provider by dependency injection and configured so that the views are cleaned up when the activity
* is destroyed with the help of (subcomponents & custom scopes)[https://dagger.dev/dev-guide/subcomponents].
*/
object ToolbarHelper : LifecycleAwareComponent(), NavigationListener {
@OpenForMocking
class ToolbarHelper : LifecycleAwareComponent(), NavigationListener {

private var toolbarView: ToolbarView? = null

Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
package com.wealthfront.magellan.sample.migration;

import com.wealthfront.magellan.sample.migration.tide.DogBreedsStepTest;
import com.wealthfront.magellan.sample.migration.tide.DogDetailsScreenTest;
import com.wealthfront.magellan.sample.migration.tide.HelpScreenTest;

import javax.inject.Singleton;
Expand All @@ -10,4 +12,8 @@
@Singleton
public interface TestAppComponent extends AppComponent {
void inject(HelpScreenTest test);

void inject(DogBreedsStepTest test);

void inject(DogDetailsScreenTest test);
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

import com.wealthfront.magellan.navigation.NavigationTraverser;
import com.wealthfront.magellan.sample.migration.api.DogApi;
import com.wealthfront.magellan.sample.migration.toolbar.ToolbarHelper;

import org.mockito.Mockito;

Expand Down Expand Up @@ -31,4 +32,9 @@ DogApi provideDogApi() {
return Mockito.mock(DogApi.class);
}

@Provides
@Singleton
ToolbarHelper provideToolbarHelper() {
return Mockito.mock(ToolbarHelper.class);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
@file:Suppress("UNCHECKED_CAST")

package com.wealthfront.magellan.sample.migration

import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.runBlocking
import org.mockito.Mockito.verify
import org.mockito.Mockito.`when`
import org.mockito.stubbing.LenientStubber
import org.mockito.stubbing.OngoingStubbing
import org.mockito.verification.VerificationMode

fun <T> LenientStubber.coWhen(block: suspend CoroutineScope.() -> T): OngoingStubbing<T> =
runBlocking {
this@coWhen.`when`(block())
}

fun <T> coWhen(block: suspend CoroutineScope.() -> T): OngoingStubbing<T> =
runBlocking {
`when`(block())
}

fun <T> coVerify(mock: T, block: suspend CoroutineScope.(T) -> Unit) {
runBlocking {
block(verify(mock))
}
}

fun <T> coVerify(mock: T, mode: VerificationMode, block: suspend CoroutineScope.(T) -> Unit) {
runBlocking {
block(verify(mock, mode))
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
package com.wealthfront.magellan.sample.migration.tide

import android.app.Activity
import android.app.Application
import android.os.Looper
import androidx.test.core.app.ApplicationProvider.getApplicationContext
import com.google.common.truth.Truth.assertThat
import com.wealthfront.magellan.lifecycle.LifecycleState
import com.wealthfront.magellan.lifecycle.transitionToState
import com.wealthfront.magellan.sample.migration.AppComponentContainer
import com.wealthfront.magellan.sample.migration.TestAppComponent
import com.wealthfront.magellan.sample.migration.api.DogApi
import com.wealthfront.magellan.sample.migration.api.DogBreeds
import com.wealthfront.magellan.sample.migration.coWhen
import org.junit.Before
import org.junit.Rule
import org.junit.Test
import org.junit.runner.RunWith
import org.mockito.junit.MockitoJUnit
import org.mockito.junit.MockitoRule
import org.mockito.quality.Strictness
import org.robolectric.Robolectric
import org.robolectric.RobolectricTestRunner
import org.robolectric.Shadows.shadowOf
import javax.inject.Inject

@RunWith(RobolectricTestRunner::class)
class DogBreedsStepTest {

private val dogBreedsStep = DogBreedsStep()
private val activity = Robolectric.buildActivity(Activity::class.java).get()

@Inject lateinit var api: DogApi

@Rule @JvmField
val mockitoRule: MockitoRule = MockitoJUnit.rule().strictness(Strictness.WARN)

@Before
fun setup() {
val context = getApplicationContext<Application>()
((context as AppComponentContainer).injector() as TestAppComponent).inject(this)

val breedData = DogBreeds(
message = listOf("chesapeake", "curly", "flatcoated", "golden"),
status = "success"
)
coWhen { api.getListOfAllBreedsOfRetriever() }.thenReturn(breedData)
}

@Test
fun fetchesDogBreedsOnShow() {
dogBreedsStep.transitionToState(LifecycleState.Shown(activity))
shadowOf(Looper.getMainLooper()).idle()
assertThat(dogBreedsStep.viewBinding!!.dogBreeds.adapter).isNotNull()
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
package com.wealthfront.magellan.sample.migration.tide

import android.app.Application
import android.content.Context
import android.os.Looper.getMainLooper
import androidx.activity.ComponentActivity
import androidx.test.core.app.ApplicationProvider
import com.wealthfront.magellan.lifecycle.LifecycleState
import com.wealthfront.magellan.lifecycle.transitionToState
import com.wealthfront.magellan.sample.migration.AppComponentContainer
import com.wealthfront.magellan.sample.migration.TestAppComponent
import com.wealthfront.magellan.sample.migration.api.DogApi
import com.wealthfront.magellan.sample.migration.api.DogMessage
import io.reactivex.Observable
import org.junit.Before
import org.junit.Rule
import org.junit.Test
import org.junit.runner.RunWith
import org.mockito.Mock
import org.mockito.Mockito.verify
import org.mockito.Mockito.`when`
import org.mockito.junit.MockitoJUnit
import org.mockito.junit.MockitoRule
import org.mockito.quality.Strictness
import org.robolectric.Robolectric.buildActivity
import org.robolectric.RobolectricTestRunner
import org.robolectric.Shadows.shadowOf
import javax.inject.Inject

@RunWith(RobolectricTestRunner::class)
class DogDetailsScreenTest {
private lateinit var screen: DogDetailsScreen
private val activity = buildActivity(ComponentActivity::class.java).get()
private val breedData = DogMessage(
message = "image-url",
status = "success"
)

@Inject lateinit var api: DogApi
@Mock lateinit var dogDetailsView: DogDetailsView

@Rule @JvmField
val mockitoRule: MockitoRule = MockitoJUnit.rule().strictness(Strictness.WARN)

@Before
fun setup() {
val context = ApplicationProvider.getApplicationContext<Application>()
((context as AppComponentContainer).injector() as TestAppComponent).inject(this)

screen = object : DogDetailsScreen("robotic") {
override fun createView(context: Context): DogDetailsView {
super.createView(context)
return dogDetailsView
}
}

`when`(api.getRandomImageForBreed("robotic")).thenReturn(Observable.just(breedData))
}

@Test
fun fetchesDogBreedOnShow() {
screen.transitionToState(LifecycleState.Shown(activity))
shadowOf(getMainLooper()).idle()
verify(dogDetailsView).setDogPic("image-url")
}
}

0 comments on commit 9d57a03

Please sign in to comment.