Skip to content

Commit

Permalink
Refactoring android tests to use utils/android (pt4). (#396)
Browse files Browse the repository at this point in the history
* Refactoring android tests to use utils/android (pt4).

* Fix coverage.
  • Loading branch information
Laimiux authored Sep 20, 2024
1 parent 3355d53 commit feeac6c
Show file tree
Hide file tree
Showing 18 changed files with 264 additions and 188 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -2,60 +2,15 @@ package com.instacart.formula.test

import android.os.Bundle
import androidx.annotation.VisibleForTesting
import com.instacart.formula.FormulaAndroid
import com.instacart.formula.android.FormulaFragment
import com.instacart.formula.android.FragmentKey
import com.instacart.formula.android.FormulaAppCompatActivity
import com.instacart.testutils.android.R

class TestFragmentActivity : FormulaAppCompatActivity() {
@VisibleForTesting lateinit var initialKey: FragmentKey
@VisibleForTesting val renderCalls = mutableListOf<Pair<FragmentKey, *>>()

override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.test_activity)

if (savedInstanceState == null) {
val fragment = FormulaFragment.newInstance(initialKey)
supportFragmentManager.beginTransaction()
.add(R.id.activity_content, fragment, initialKey.tag)
.addToBackStack(initialKey.tag)
.commit()
}
}

fun navigateTo(key: FragmentKey, allowStateLoss: Boolean = false) {
val entryIndex = supportFragmentManager.backStackEntryCount - 1
val fragment = if (entryIndex >= 0) {
val entry = supportFragmentManager.getBackStackEntryAt(entryIndex)
supportFragmentManager.findFragmentByTag(entry.name)
} else {
null
}

supportFragmentManager.beginTransaction().apply {
if (fragment != null) {
remove(fragment)
}
add(R.id.activity_content, FormulaFragment.newInstance(key), key.tag)
addToBackStack(key.tag)
}.apply {
if (allowStateLoss) {
commitAllowingStateLoss()
} else {
commit()
}
}
}

override fun onBackPressed() {
if (!FormulaAndroid.onBackPressed(this)) {
if (supportFragmentManager.backStackEntryCount > 1) {
supportFragmentManager.popBackStackImmediate()
} else {
finish()
}
}
}
}
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
package com.instacart.formula

import android.os.Looper
import androidx.fragment.app.Fragment
import androidx.lifecycle.Lifecycle
import androidx.test.core.app.ActivityScenario
import androidx.test.ext.junit.rules.ActivityScenarioRule
Expand All @@ -16,12 +15,14 @@ import com.instacart.formula.android.FragmentEnvironment
import com.instacart.formula.android.FragmentStore
import com.instacart.formula.android.events.FragmentLifecycleEvent
import com.instacart.formula.test.TestBackCallbackRenderModel
import com.instacart.formula.test.TestKey
import com.instacart.testutils.android.TestKey
import com.instacart.formula.test.TestKeyWithId
import com.instacart.formula.test.TestFragmentActivity
import com.instacart.formula.test.TestLifecycleKey
import com.instacart.testutils.android.HeadlessFragment
import com.instacart.testutils.android.activity
import com.instacart.testutils.android.get
import com.instacart.testutils.android.showFragment
import com.jakewharton.rxrelay3.PublishRelay
import io.reactivex.rxjava3.core.Observable
import org.junit.Before
Expand All @@ -38,11 +39,8 @@ import java.util.concurrent.TimeUnit
@RunWith(AndroidJUnit4::class)
class FormulaFragmentTest {

class HeadlessFragment : Fragment()

private var lastState: FragmentState? = null
private val stateChangeRelay = PublishRelay.create<Pair<FragmentKey, Any>>()
private var onPreCreated: (TestFragmentActivity) -> Unit = {}
private var updateThreads = linkedSetOf<Thread>()
private val errors = mutableListOf<Throwable>()
private val fragmentLifecycleEvents = mutableListOf<FragmentLifecycleEvent>()
Expand All @@ -56,10 +54,6 @@ class FormulaFragmentTest {
FormulaAndroid.init(app, environment) {
activity<TestFragmentActivity> {
ActivityStore(
configureActivity = { activity ->
activity.initialKey = TestKey()
onPreCreated(activity)
},
onRenderFragmentState = { a, state ->
lastState = state

Expand Down Expand Up @@ -100,6 +94,7 @@ class FormulaFragmentTest {

@Before fun setup() {
scenario = activityRule.scenario
scenario.showFragment(TestKey())
}

@Test fun `add fragment lifecycle event`() {
Expand Down Expand Up @@ -257,13 +252,13 @@ class FormulaFragmentTest {

// Pass feature updates on a background thread
executor.execute {
stateChangeRelay.accept(initial to "main-state-1")
stateChangeRelay.accept(initial to "main-state-2")
stateChangeRelay.accept(initial to "main-state-3")
sendStateUpdate(initial, "main-state-1")
sendStateUpdate(initial, "main-state-2")
sendStateUpdate(initial, "main-state-3")

stateChangeRelay.accept(keyWithId to "detail-state-1")
stateChangeRelay.accept(keyWithId to "detail-state-2")
stateChangeRelay.accept(keyWithId to "detail-state-3")
sendStateUpdate(keyWithId, "detail-state-1")
sendStateUpdate(keyWithId, "detail-state-2")
sendStateUpdate(keyWithId, "detail-state-3")
latch.countDown()
}

Expand Down Expand Up @@ -344,9 +339,8 @@ class FormulaFragmentTest {
}

private fun navigateToTaskDetail(id: Int = 1, allowStateLoss: Boolean = false) {
scenario.onActivity {
it.navigateTo(TestKeyWithId(id), allowStateLoss = allowStateLoss)
}
val fragmentKey = TestKeyWithId(id)
scenario.showFragment(fragmentKey, allowStateLoss)
}

private fun assertFragmentViewIsCreated(key: FragmentKey) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,32 +3,27 @@ package com.instacart.formula
import androidx.test.core.app.ActivityScenario
import androidx.test.ext.junit.runners.AndroidJUnit4
import com.instacart.formula.android.ActivityStore
import com.instacart.formula.android.FormulaFragment
import com.instacart.formula.android.FragmentStore
import com.instacart.formula.android.FragmentKey
import com.instacart.formula.test.TestKey
import com.instacart.testutils.android.TestKey
import com.instacart.formula.test.TestKeyWithId
import com.instacart.formula.test.TestFragmentActivity
import com.instacart.testutils.android.FormulaAndroidInteractor
import com.instacart.testutils.android.NoOpFeatureFactory
import com.instacart.testutils.android.TestFormulaActivity
import com.instacart.testutils.android.showFragment
import com.instacart.testutils.android.withFormulaAndroid
import io.reactivex.rxjava3.observers.TestObserver
import org.junit.Test
import org.junit.runner.RunWith
import org.robolectric.annotation.LooperMode
import com.instacart.testutils.android.R as TestR

@RunWith(AndroidJUnit4::class)
class FragmentLifecycleStateTest {

private fun runTest(continuation: (FormulaAndroidInteractor) -> Unit) {
withFormulaAndroid(
configure = {
activity<TestFragmentActivity> {
activity<TestFormulaActivity> {
ActivityStore(
configureActivity = {
it.initialKey = TestKey()
},
fragmentStore = FragmentStore.init {
bind(NoOpFeatureFactory<TestKey>())
bind(NoOpFeatureFactory<TestKeyWithId>())
Expand All @@ -44,34 +39,41 @@ class FragmentLifecycleStateTest {
@Test
fun `is fragment started`() {
runTest { interactor ->
val startedEvents = interactor.startedEvents(TestKey())
ActivityScenario.launch(TestFragmentActivity::class.java)
val fragmentKey = TestKey()
val startedEvents = interactor.startedEvents(fragmentKey)

val scenario = ActivityScenario.launch(TestFormulaActivity::class.java)
scenario.showFragment(fragmentKey)

startedEvents.assertValues(false, true)
}
}

@Test
fun `is fragment resumed`() {
runTest { interactor ->
val resumedEvents = interactor.resumedEvents(TestKey())
ActivityScenario.launch(TestFragmentActivity::class.java)
val fragmentKey = TestKey()
val resumedEvents = interactor.resumedEvents(fragmentKey)
val scenario = ActivityScenario.launch(TestFormulaActivity::class.java)
scenario.showFragment(fragmentKey)
resumedEvents.assertValues(false, true)
}
}

@LooperMode(LooperMode.Mode.LEGACY)
@Test
fun `navigate forward`() {
runTest { interactor ->
val initialKeyStartedEvents = interactor.startedEvents(TestKey())
val initialKeyResumedEvents = interactor.resumedEvents(TestKey())
val initialKey = TestKey()
val initialKeyStartedEvents = interactor.startedEvents(initialKey)
val initialKeyResumedEvents = interactor.resumedEvents(initialKey)

val detailKey = TestKeyWithId(1)
val detailKeyStartedEvents = interactor.startedEvents(detailKey)
val detailKeyResumedEvents = interactor.resumedEvents(detailKey)

val scenario = ActivityScenario.launch(TestFragmentActivity::class.java)
navigateToTaskDetail(scenario, detailKey)
val scenario = ActivityScenario.launch(TestFormulaActivity::class.java)
scenario.showFragment(initialKey)
scenario.showFragment(detailKey)

initialKeyStartedEvents.assertValues(false, true, false)
initialKeyResumedEvents.assertValues(false, true, false)
Expand All @@ -88,17 +90,4 @@ class FragmentLifecycleStateTest {
private fun FormulaAndroidInteractor.resumedEvents(key: FragmentKey): TestObserver<Boolean> {
return selectEvents { it.isFragmentResumed(key) }.test()
}

private fun navigateToTaskDetail(
scenario: ActivityScenario<TestFragmentActivity>,
key: TestKeyWithId,
) {
scenario.onActivity {
it.supportFragmentManager.beginTransaction()
.remove(it.supportFragmentManager.findFragmentByTag(TestKey().tag)!!)
.add(TestR.id.activity_content, FormulaFragment.newInstance(key), key.tag)
.addToBackStack(null)
.commit()
}
}
}
Original file line number Diff line number Diff line change
@@ -1,93 +1,85 @@
package com.instacart.formula

import android.content.Context
import android.content.Intent
import android.os.Bundle
import androidx.test.core.app.ActivityScenario
import androidx.test.core.app.ApplicationProvider
import androidx.test.ext.junit.runners.AndroidJUnit4
import com.google.common.truth.Truth.assertThat
import com.instacart.formula.android.ActivityStore
import com.instacart.formula.android.Feature
import com.instacart.formula.android.FeatureFactory
import com.instacart.formula.android.FormulaFragment
import com.instacart.formula.android.FragmentStore
import com.instacart.formula.android.ViewFactory
import com.instacart.formula.test.TestFragmentActivity
import com.instacart.formula.test.TestFragmentLifecycleCallback
import com.instacart.testutils.android.TestFragmentLifecycleCallback
import com.instacart.formula.test.TestLifecycleKey
import com.instacart.testutils.android.R as TestR
import io.reactivex.rxjava3.core.Observable
import org.junit.Rule
import com.instacart.testutils.android.NoOpFeatureFactory
import com.instacart.testutils.android.TestFormulaActivity
import com.instacart.testutils.android.TestViewFactory
import com.instacart.testutils.android.showFragment
import com.instacart.testutils.android.withFormulaAndroid
import org.junit.Test
import org.junit.runner.RunWith

@RunWith(AndroidJUnit4::class)
class FragmentLifecycleTest {

private lateinit var lifecycleCallback: TestFragmentLifecycleCallback

@get:Rule val formulaRule = TestFormulaRule(initFormula = { app ->
FormulaAndroid.init(app) {
activity<TestFragmentActivity> {
lifecycleCallback = TestFragmentLifecycleCallback()
ActivityStore(
configureActivity = { activity ->
activity.initialKey = TestLifecycleKey()
},
fragmentStore = FragmentStore.init {
val featureFactory = object : FeatureFactory<Unit, TestLifecycleKey> {
override fun initialize(dependencies: Unit, key: TestLifecycleKey): Feature {
return Feature(
state = Observable.empty(),
viewFactory = ViewFactory.fromLayout(TestR.layout.test_fragment_layout) {
featureView(lifecycleCallback) {}
}
)
}
}
bind(featureFactory)
},
)
private fun runTest(
continuation: (ActivityScenario<TestFormulaActivity>, TestFragmentLifecycleCallback) -> Unit
) {
val lifecycleCallback = TestFragmentLifecycleCallback()
withFormulaAndroid(
configure = {
activity<TestFormulaActivity> {
ActivityStore(
fragmentStore = FragmentStore.init {
val featureFactory = NoOpFeatureFactory<TestLifecycleKey>(
viewFactory = TestViewFactory(lifecycleCallback)
)
bind(featureFactory)
},
)
}
}
) {
val scenario = ActivityScenario.launch(TestFormulaActivity::class.java)
scenario.showFragment(TestLifecycleKey())
continuation(scenario, lifecycleCallback)
}
})
}

@Test fun `creation callbacks`() {
ActivityScenario.launch(TestFragmentActivity::class.java)

assertThat(lifecycleCallback.hasOnViewCreated).isTrue()
assertThat(lifecycleCallback.hasOnActivityCreated).isTrue()
assertThat(lifecycleCallback.hasOnStart).isTrue()
assertThat(lifecycleCallback.hasOnResume).isTrue()
runTest { _, lifecycleCallback ->
assertThat(lifecycleCallback.hasOnViewCreated).isTrue()
assertThat(lifecycleCallback.hasOnActivityCreated).isTrue()
assertThat(lifecycleCallback.hasOnStart).isTrue()
assertThat(lifecycleCallback.hasOnResume).isTrue()
}
}

@Test fun `destroy callbacks`() {
val scenario = ActivityScenario.launch(TestFragmentActivity::class.java)
scenario.close()
runTest { scenario, lifecycleCallback ->
scenario.close()

assertThat(lifecycleCallback.hasOnPauseEvent).isTrue()
assertThat(lifecycleCallback.hasOnStop).isTrue()
assertThat(lifecycleCallback.hasOnDestroyView).isTrue()
assertThat(lifecycleCallback.hasOnPauseEvent).isTrue()
assertThat(lifecycleCallback.hasOnStop).isTrue()
assertThat(lifecycleCallback.hasOnDestroyView).isTrue()
}
}

@Test fun `save instance state callback`() {
val scenario = ActivityScenario.launch(TestFragmentActivity::class.java)

assertThat(lifecycleCallback.hasOnSaveInstanceState).isFalse()
scenario.recreate()
assertThat(lifecycleCallback.hasOnSaveInstanceState).isTrue()
runTest { scenario, lifecycleCallback ->
assertThat(lifecycleCallback.hasOnSaveInstanceState).isFalse()
scenario.recreate()
assertThat(lifecycleCallback.hasOnSaveInstanceState).isTrue()
}
}

@Test fun `low memory`() {
val scenario = ActivityScenario.launch(TestFragmentActivity::class.java)
scenario.onActivity {
val fragment = it.supportFragmentManager.fragments
.filterIsInstance<FormulaFragment>()
.first()
runTest { scenario, lifecycleCallback ->
scenario.onActivity {
val fragment = it.supportFragmentManager.fragments
.filterIsInstance<FormulaFragment>()
.first()

fragment.onLowMemory()
fragment.onLowMemory()
}
assertThat(lifecycleCallback.hasCalledLowMemory).isTrue()
}
assertThat(lifecycleCallback.hasCalledLowMemory).isTrue()
}
}
Loading

0 comments on commit feeac6c

Please sign in to comment.