From 98b71fb44482b2970283137e1ba6d0f450141b16 Mon Sep 17 00:00:00 2001 From: Laimonas Turauskas Date: Mon, 23 Sep 2024 11:27:29 -0400 Subject: [PATCH] Refactoring android tests to use utils/android (pt5). (#397) --- .../src/main/AndroidManifest.xml | 7 -- .../formula/test/TestFragmentActivity.kt | 16 ---- .../formula/test/TestLifecycleKey.kt | 9 -- .../instacart/formula/FormulaFragmentTest.kt | 52 ++++++------ .../formula/FragmentLifecycleTest.kt | 85 ------------------- .../instacart/formula/TestFeatureFactory.kt | 9 +- .../formula/android/FormulaAndroidTest.kt | 41 ++++++++- .../test/FeatureFactoryLifecycleInteractor.kt | 42 +++++++++ 8 files changed, 112 insertions(+), 149 deletions(-) delete mode 100644 formula-android-tests/src/main/java/com/instacart/formula/test/TestFragmentActivity.kt delete mode 100644 formula-android-tests/src/main/java/com/instacart/formula/test/TestLifecycleKey.kt delete mode 100644 formula-android-tests/src/test/java/com/instacart/formula/FragmentLifecycleTest.kt create mode 100644 formula-android/src/test/java/com/instacart/formula/android/test/FeatureFactoryLifecycleInteractor.kt diff --git a/formula-android-tests/src/main/AndroidManifest.xml b/formula-android-tests/src/main/AndroidManifest.xml index 2a6798b7..98ced030 100644 --- a/formula-android-tests/src/main/AndroidManifest.xml +++ b/formula-android-tests/src/main/AndroidManifest.xml @@ -6,12 +6,5 @@ android:roundIcon="@mipmap/ic_launcher_round" android:supportsRtl="true" android:theme="@style/AppTheme"> - - - - - - - diff --git a/formula-android-tests/src/main/java/com/instacart/formula/test/TestFragmentActivity.kt b/formula-android-tests/src/main/java/com/instacart/formula/test/TestFragmentActivity.kt deleted file mode 100644 index eb17abf9..00000000 --- a/formula-android-tests/src/main/java/com/instacart/formula/test/TestFragmentActivity.kt +++ /dev/null @@ -1,16 +0,0 @@ -package com.instacart.formula.test - -import android.os.Bundle -import androidx.annotation.VisibleForTesting -import com.instacart.formula.android.FragmentKey -import com.instacart.formula.android.FormulaAppCompatActivity -import com.instacart.testutils.android.R - -class TestFragmentActivity : FormulaAppCompatActivity() { - @VisibleForTesting val renderCalls = mutableListOf>() - - override fun onCreate(savedInstanceState: Bundle?) { - super.onCreate(savedInstanceState) - setContentView(R.layout.test_activity) - } -} diff --git a/formula-android-tests/src/main/java/com/instacart/formula/test/TestLifecycleKey.kt b/formula-android-tests/src/main/java/com/instacart/formula/test/TestLifecycleKey.kt deleted file mode 100644 index 9efacaf3..00000000 --- a/formula-android-tests/src/main/java/com/instacart/formula/test/TestLifecycleKey.kt +++ /dev/null @@ -1,9 +0,0 @@ -package com.instacart.formula.test - -import com.instacart.formula.android.FragmentKey -import kotlinx.parcelize.Parcelize - -@Parcelize -data class TestLifecycleKey( - override val tag: String = "test-lifecycle", -) : FragmentKey diff --git a/formula-android-tests/src/test/java/com/instacart/formula/FormulaFragmentTest.kt b/formula-android-tests/src/test/java/com/instacart/formula/FormulaFragmentTest.kt index f1c0dced..2d1697a1 100644 --- a/formula-android-tests/src/test/java/com/instacart/formula/FormulaFragmentTest.kt +++ b/formula-android-tests/src/test/java/com/instacart/formula/FormulaFragmentTest.kt @@ -17,11 +17,9 @@ import com.instacart.formula.android.events.FragmentLifecycleEvent import com.instacart.formula.test.TestBackCallbackRenderModel 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.TestFormulaActivity 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 @@ -31,7 +29,6 @@ import org.junit.Test import org.junit.rules.RuleChain import org.junit.runner.RunWith import org.robolectric.Shadows -import org.robolectric.annotation.LooperMode import java.util.concurrent.CountDownLatch import java.util.concurrent.Executors import java.util.concurrent.TimeUnit @@ -44,6 +41,8 @@ class FormulaFragmentTest { private var updateThreads = linkedSetOf() private val errors = mutableListOf() private val fragmentLifecycleEvents = mutableListOf() + private val renderCalls = mutableListOf>() + private val formulaRule = TestFormulaRule( initFormula = { app -> val environment = FragmentEnvironment( @@ -52,7 +51,7 @@ class FormulaFragmentTest { } ) FormulaAndroid.init(app, environment) { - activity { + activity { ActivityStore( onRenderFragmentState = { a, state -> lastState = state @@ -60,9 +59,17 @@ class FormulaFragmentTest { updateThreads.add(Thread.currentThread()) }, fragmentStore = FragmentStore.init { - bind(TestFeatureFactory { stateChanges(it) }) + bind( + featureFactory = TestFeatureFactory( + render = { key, value -> + renderCalls.add(key to value) + }, + state = { stateChanges(it) } + ) + ) bind(TestFeatureFactory( - applyOutput = { output -> + render = { key, output -> + renderCalls.add(key to output) if (output == "crash") { throw IllegalStateException("crashing") } @@ -87,10 +94,10 @@ class FormulaFragmentTest { } ) - private val activityRule = ActivityScenarioRule(TestFragmentActivity::class.java) + private val activityRule = ActivityScenarioRule(TestFormulaActivity::class.java) @get:Rule val rule = RuleChain.outerRule(formulaRule).around(activityRule) - lateinit var scenario: ActivityScenario + lateinit var scenario: ActivityScenario @Before fun setup() { scenario = activityRule.scenario @@ -136,17 +143,15 @@ class FormulaFragmentTest { } @Test fun `render model is passed to visible fragment`() { - val activity = activity() sendStateUpdate(TestKey(), "update") - assertThat(activity.renderCalls).containsExactly(TestKey() to "update").inOrder() + assertThat(renderCalls).containsExactly(TestKey() to "update").inOrder() } @Test fun `render model is not passed to not visible fragment`() { navigateToTaskDetail() - val activity = activity() sendStateUpdate(TestKey(), "update") - assertThat(activity.renderCalls).isEqualTo(emptyList()) + assertThat(renderCalls).isEqualTo(emptyList()) } @Test fun `visible fragments are updated when navigating`() { @@ -154,17 +159,15 @@ class FormulaFragmentTest { val contract = TestKeyWithId(1) - val activity = activity() sendStateUpdate(contract, "update") - assertThat(activity.renderCalls).containsExactly(contract to "update").inOrder() + assertThat(renderCalls).containsExactly(contract to "update").inOrder() navigateBack() sendStateUpdate(contract, "update-two") - assertThat(activity.renderCalls).containsExactly(contract to "update").inOrder() + assertThat(renderCalls).containsExactly(contract to "update").inOrder() } - @LooperMode(LooperMode.Mode.LEGACY) @Test fun `delegates back press to current render model`() { navigateToTaskDetail() @@ -318,19 +321,18 @@ class FormulaFragmentTest { val key = TestKeyWithId(1) navigateToTaskDetail(id = key.id) - val activity = activity() sendStateUpdate(key, "crash") - assertThat(activity.renderCalls).isNotEmpty() + assertThat(renderCalls).isNotEmpty() assertThat(errors).hasSize(1) } @Test fun toStringContainsTagAndKey() { - val fragment = FormulaFragment.newInstance(TestLifecycleKey()) + val fragment = FormulaFragment.newInstance(TestKey()) val toStringValue = fragment.toString() assertThat(toStringValue).isEqualTo( - "test-lifecycle -> TestLifecycleKey(tag=test-lifecycle)" + "test key -> TestKey(tag=test key)" ) } @@ -350,20 +352,18 @@ class FormulaFragmentTest { } } - private fun activity(): TestFragmentActivity { + private fun activity(): TestFormulaActivity { return scenario.activity() } private fun activeContracts(): List { - return scenario.get { - lastState!!.activeIds.map { it.key } - } + return lastState!!.activeIds.map { it.key } } private fun assertVisibleContract(contract: FragmentKey) { assertNoDuplicates(contract) // TODO: would be best to test visibleState() however `FragmentFlowState.states` is empty - assertThat(scenario.get { lastState?.visibleIds?.lastOrNull()?.key }).isEqualTo(contract) + assertThat(lastState?.visibleIds?.lastOrNull()?.key).isEqualTo(contract) } private fun assertNoDuplicates(contract: FragmentKey) { diff --git a/formula-android-tests/src/test/java/com/instacart/formula/FragmentLifecycleTest.kt b/formula-android-tests/src/test/java/com/instacart/formula/FragmentLifecycleTest.kt deleted file mode 100644 index bc7dfa9f..00000000 --- a/formula-android-tests/src/test/java/com/instacart/formula/FragmentLifecycleTest.kt +++ /dev/null @@ -1,85 +0,0 @@ -package com.instacart.formula - -import androidx.test.core.app.ActivityScenario -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.FormulaFragment -import com.instacart.formula.android.FragmentStore -import com.instacart.testutils.android.TestFragmentLifecycleCallback -import com.instacart.formula.test.TestLifecycleKey -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 fun runTest( - continuation: (ActivityScenario, TestFragmentLifecycleCallback) -> Unit - ) { - val lifecycleCallback = TestFragmentLifecycleCallback() - withFormulaAndroid( - configure = { - activity { - ActivityStore( - fragmentStore = FragmentStore.init { - val featureFactory = NoOpFeatureFactory( - viewFactory = TestViewFactory(lifecycleCallback) - ) - bind(featureFactory) - }, - ) - } - } - ) { - val scenario = ActivityScenario.launch(TestFormulaActivity::class.java) - scenario.showFragment(TestLifecycleKey()) - continuation(scenario, lifecycleCallback) - } - } - - @Test fun `creation callbacks`() { - runTest { _, lifecycleCallback -> - assertThat(lifecycleCallback.hasOnViewCreated).isTrue() - assertThat(lifecycleCallback.hasOnActivityCreated).isTrue() - assertThat(lifecycleCallback.hasOnStart).isTrue() - assertThat(lifecycleCallback.hasOnResume).isTrue() - } - } - - @Test fun `destroy callbacks`() { - runTest { scenario, lifecycleCallback -> - scenario.close() - - assertThat(lifecycleCallback.hasOnPauseEvent).isTrue() - assertThat(lifecycleCallback.hasOnStop).isTrue() - assertThat(lifecycleCallback.hasOnDestroyView).isTrue() - } - } - - @Test fun `save instance state callback`() { - runTest { scenario, lifecycleCallback -> - assertThat(lifecycleCallback.hasOnSaveInstanceState).isFalse() - scenario.recreate() - assertThat(lifecycleCallback.hasOnSaveInstanceState).isTrue() - } - } - - @Test fun `low memory`() { - runTest { scenario, lifecycleCallback -> - scenario.onActivity { - val fragment = it.supportFragmentManager.fragments - .filterIsInstance() - .first() - - fragment.onLowMemory() - } - assertThat(lifecycleCallback.hasCalledLowMemory).isTrue() - } - } -} diff --git a/formula-android-tests/src/test/java/com/instacart/formula/TestFeatureFactory.kt b/formula-android-tests/src/test/java/com/instacart/formula/TestFeatureFactory.kt index b771c670..c92f5ff4 100644 --- a/formula-android-tests/src/test/java/com/instacart/formula/TestFeatureFactory.kt +++ b/formula-android-tests/src/test/java/com/instacart/formula/TestFeatureFactory.kt @@ -3,20 +3,19 @@ package com.instacart.formula import com.instacart.formula.android.Feature import com.instacart.formula.android.FeatureFactory import com.instacart.formula.android.FragmentKey -import com.instacart.formula.test.TestFragmentActivity import com.instacart.testutils.android.TestViewFactory import io.reactivex.rxjava3.core.Observable class TestFeatureFactory( - private val applyOutput: (Any) -> Unit = {}, + private val render: (FragmentKey, Any) -> Unit = { _, _ -> }, private val state: (Key) -> Observable, + ) : FeatureFactory { override fun initialize(dependencies: Unit, key: Key): Feature { return Feature( state = state(key), - viewFactory = TestViewFactory { view, value -> - (view.context as TestFragmentActivity).renderCalls.add(Pair(key, value)) - applyOutput(value) + viewFactory = TestViewFactory { _, value -> + render(key, value) } ) } diff --git a/formula-android/src/test/java/com/instacart/formula/android/FormulaAndroidTest.kt b/formula-android/src/test/java/com/instacart/formula/android/FormulaAndroidTest.kt index 57d82ffa..a30fe832 100644 --- a/formula-android/src/test/java/com/instacart/formula/android/FormulaAndroidTest.kt +++ b/formula-android/src/test/java/com/instacart/formula/android/FormulaAndroidTest.kt @@ -13,6 +13,7 @@ import com.google.common.truth.Truth.assertThat import com.instacart.formula.FormulaAndroid import com.instacart.formula.android.events.ActivityResult import com.instacart.formula.android.test.runActivityUpdateTest +import com.instacart.formula.android.test.runFeatureFactoryLifecycleTest import com.instacart.testutils.android.TestActivity import com.instacart.testutils.android.TestFormulaActivity import com.instacart.testutils.android.TestFragmentActivity @@ -27,7 +28,6 @@ class FormulaAndroidTest { @Test fun `crashes if initialized twice`() { - try { val result = runCatching { val context = ApplicationProvider.getApplicationContext() @@ -180,4 +180,43 @@ class FormulaAndroidTest { ) } } + + @Test + fun `feature factory lifecycle events`() { + runFeatureFactoryLifecycleTest { + assertThat(lifecycleCallback.hasOnViewCreated).isTrue() + assertThat(lifecycleCallback.hasOnActivityCreated).isTrue() + assertThat(lifecycleCallback.hasOnStart).isTrue() + assertThat(lifecycleCallback.hasOnResume).isTrue() + + scenario.close() + + assertThat(lifecycleCallback.hasOnPauseEvent).isTrue() + assertThat(lifecycleCallback.hasOnStop).isTrue() + assertThat(lifecycleCallback.hasOnDestroyView).isTrue() + } + } + + @Test + fun `feature factory save instance event`() { + runFeatureFactoryLifecycleTest { + assertThat(lifecycleCallback.hasOnSaveInstanceState).isFalse() + scenario.recreate() + assertThat(lifecycleCallback.hasOnSaveInstanceState).isTrue() + } + } + + @Test + fun `feature factory low memory event`() { + runFeatureFactoryLifecycleTest { + scenario.onActivity { + val fragment = it.supportFragmentManager.fragments + .filterIsInstance() + .first() + + fragment.onLowMemory() + } + assertThat(lifecycleCallback.hasCalledLowMemory).isTrue() + } + } } diff --git a/formula-android/src/test/java/com/instacart/formula/android/test/FeatureFactoryLifecycleInteractor.kt b/formula-android/src/test/java/com/instacart/formula/android/test/FeatureFactoryLifecycleInteractor.kt new file mode 100644 index 00000000..17387956 --- /dev/null +++ b/formula-android/src/test/java/com/instacart/formula/android/test/FeatureFactoryLifecycleInteractor.kt @@ -0,0 +1,42 @@ +package com.instacart.formula.android.test + +import androidx.test.core.app.ActivityScenario +import com.instacart.formula.android.ActivityStore +import com.instacart.formula.android.FragmentStore +import com.instacart.testutils.android.NoOpFeatureFactory +import com.instacart.testutils.android.TestFormulaActivity +import com.instacart.testutils.android.TestFragmentLifecycleCallback +import com.instacart.testutils.android.TestKey +import com.instacart.testutils.android.TestViewFactory +import com.instacart.testutils.android.showFragment +import com.instacart.testutils.android.withFormulaAndroid + +class FeatureFactoryLifecycleInteractor( + val scenario: ActivityScenario, + val lifecycleCallback: TestFragmentLifecycleCallback, +) + +fun runFeatureFactoryLifecycleTest( + continuation: FeatureFactoryLifecycleInteractor.() -> Unit +) { + val lifecycleCallback = TestFragmentLifecycleCallback() + withFormulaAndroid( + configure = { + activity { + ActivityStore( + fragmentStore = FragmentStore.init { + val featureFactory = NoOpFeatureFactory( + viewFactory = TestViewFactory(lifecycleCallback) + ) + bind(featureFactory) + }, + ) + } + } + ) { + val scenario = ActivityScenario.launch(TestFormulaActivity::class.java) + scenario.showFragment(TestKey()) + val interactor = FeatureFactoryLifecycleInteractor(scenario, lifecycleCallback) + continuation(interactor) + } +} \ No newline at end of file