Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Increase code coverage (pt5). #392

Merged
merged 1 commit into from
Sep 18, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 2 additions & 14 deletions formula-android-tests/build.gradle.kts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
plugins {
id("com.android.application")
id("com.android.library")
id("kotlin-android")
id("kotlin-parcelize")
}
Expand All @@ -10,19 +10,6 @@ apply {

android {
namespace = "com.instacart.formula"
defaultConfig {
applicationId = "com.instacart.formula"
versionCode = 1
versionName = "1.0"
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
}

buildTypes {
release {
isMinifyEnabled = false
proguardFiles(getDefaultProguardFile("proguard-android-optimize.txt"), "proguard-rules.pro")
}
}

testOptions {
unitTests {
Expand All @@ -47,4 +34,5 @@ dependencies {
testImplementation(libs.junit)
testImplementation(libs.robolectric)
testImplementation(libs.truth)
testImplementation(project(":test-utils:android"))
}
21 changes: 0 additions & 21 deletions formula-android-tests/proguard-rules.pro

This file was deleted.

14 changes: 0 additions & 14 deletions formula-android-tests/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
Expand Up @@ -34,19 +34,5 @@
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>

<activity android:name="com.instacart.formula.NonBoundActivityTest$TestActivity">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>

<activity android:name="com.instacart.formula.NonBoundFragmentActivityTest$TestActivity">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
</application>
</manifest>
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
package com.instacart.formula

import android.app.Activity
import androidx.fragment.app.FragmentActivity
import androidx.test.core.app.ActivityScenario
import androidx.test.ext.junit.rules.ActivityScenarioRule
import androidx.test.ext.junit.runners.AndroidJUnit4
import com.instacart.testutils.android.TestActivity
import org.junit.Before
import org.junit.Rule
import org.junit.Test
Expand All @@ -16,7 +16,6 @@ import org.junit.runner.RunWith
*/
@RunWith(AndroidJUnit4::class)
class NonBoundActivityTest {
class TestActivity : Activity()

private val formulaRule = TestFormulaRule(
initFormula = { app ->
Expand Down
Original file line number Diff line number Diff line change
@@ -1,33 +1,31 @@
package com.instacart.formula

import androidx.fragment.app.FragmentActivity
import androidx.test.core.app.ActivityScenario
import androidx.test.ext.junit.rules.ActivityScenarioRule
import androidx.test.ext.junit.runners.AndroidJUnit4
import com.instacart.testutils.android.TestFragmentActivity
import org.junit.Before
import org.junit.Rule
import org.junit.Test
import org.junit.rules.RuleChain
import org.junit.runner.RunWith

/**
* Tests that formula-android module handles non-bound activities gracefully.
* Tests that formula-android module handles non-bound fragment activities gracefully.
*/
@RunWith(AndroidJUnit4::class)
class NonBoundFragmentActivityTest {
class TestActivity : FragmentActivity()

private val formulaRule = TestFormulaRule(
initFormula = { app ->
FormulaAndroid.init(app) {}
}
)

private val activityRule = ActivityScenarioRule(TestActivity::class.java)
private val activityRule = ActivityScenarioRule(TestFragmentActivity::class.java)

@get:Rule
val rule = RuleChain.outerRule(formulaRule).around(activityRule)
lateinit var scenario: ActivityScenario<TestActivity>
lateinit var scenario: ActivityScenario<TestFragmentActivity>

@Before
fun setup() {
Expand Down
2 changes: 2 additions & 0 deletions formula-android/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ android {

testOptions {
unitTests.isReturnDefaultValues = true
unitTests.isIncludeAndroidResources = true
}

publishing {
Expand All @@ -40,5 +41,6 @@ dependencies {
testImplementation(libs.mockito.kotlin)
testImplementation(libs.robolectric)
testImplementation(libs.truth)
testImplementation(project(":test-utils:android"))
}

Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,7 @@ internal class ActivityStoreContextImpl<Activity : FragmentActivity> : ActivityS
select: Activity.() -> Observable<Event>
): Observable<Event> {
// TODO: should probably use startedActivity
return activityAttachEvents()
return attachEventRelay
.switchMap {
val activity = activity
if (activity == null) {
Expand Down Expand Up @@ -105,8 +105,8 @@ internal class ActivityStoreContextImpl<Activity : FragmentActivity> : ActivityS
fun detachActivity(activity: Activity) {
if (this.activity == activity) {
this.activity = null
attachEventRelay.accept(false)
}
attachEventRelay.accept(false)
}

fun updateFragmentLifecycleState(id: FragmentId, newState: Lifecycle.State) {
Expand All @@ -121,8 +121,6 @@ internal class ActivityStoreContextImpl<Activity : FragmentActivity> : ActivityS
fragmentStateUpdated.accept(contract.tag)
}

private fun activityAttachEvents(): Observable<Boolean> = attachEventRelay

private fun fragmentLifecycleState(tag: String): Observable<Lifecycle.State> {
return fragmentStateUpdated
.filter { it == tag }
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
package com.instacart.formula.android.internal

import android.os.Looper
import androidx.fragment.app.FragmentActivity
import androidx.test.core.app.ActivityScenario
import androidx.test.ext.junit.runners.AndroidJUnit4
import com.google.common.truth.Truth.assertThat
import com.instacart.testutils.android.TestFragmentActivity
import com.instacart.testutils.android.activity
import com.instacart.testutils.android.executeOnBackgroundThread
import com.instacart.testutils.android.throwOnTimeout
import org.junit.Test
import org.junit.runner.RunWith
import java.util.concurrent.CountDownLatch

@RunWith(AndroidJUnit4::class)
class ActivityStoreContextTest {

@Test fun `started activity returns null until onActivityStarted is called`() {
val scenario = ActivityScenario.launch(TestFragmentActivity::class.java)
val storeContext = ActivityStoreContextImpl<TestFragmentActivity>()

// Initially null
assertThat(storeContext.startedActivity()).isNull()

// After attach
storeContext.attachActivity(scenario.activity())
assertThat(storeContext.startedActivity()).isNull()

// After on started
storeContext.onActivityStarted(scenario.activity())
assertThat(storeContext.startedActivity()).isEqualTo(scenario.activity())
}

@Test fun `detaches only if the activity matches`() {
val scenario = ActivityScenario.launch(TestFragmentActivity::class.java)
val storeContext = ActivityStoreContextImpl<TestFragmentActivity>()

val oldActivity = scenario.activity()
val newActivity = scenario.recreate().activity()

storeContext.attachActivity(newActivity)
storeContext.onActivityStarted(newActivity)
storeContext.detachActivity(oldActivity)

assertThat(storeContext.startedActivity()).isEqualTo(newActivity)
}

@Test fun `send posts events on the main thread`() {
val scenario = ActivityScenario.launch(TestFragmentActivity::class.java)
val storeContext = ActivityStoreContextImpl<TestFragmentActivity>()
storeContext.attachActivity(scenario.activity())
storeContext.onActivityStarted(scenario.activity())

val effectThread = mutableListOf<Looper?>()
storeContext.send { effectThread.add(Looper.myLooper()) }
storeContext.sendOnBackgroundThread { effectThread.add(Looper.myLooper()) }

assertThat(effectThread).containsExactly(
Looper.getMainLooper(), Looper.getMainLooper()
)
}

@Test
fun `send drops the action if there is no started activity`() {
val storeContext = ActivityStoreContextImpl<TestFragmentActivity>()

val effectThread = mutableListOf<Looper?>()
storeContext.send { effectThread.add(Looper.myLooper()) }

val result = runCatching {
storeContext.sendOnBackgroundThread { effectThread.add(Looper.myLooper()) }
}
assertThat(effectThread).isEmpty()
assertThat(result.exceptionOrNull()).hasMessageThat().contains(
"timeout"
)
}

private fun <ActivityType : FragmentActivity> ActivityStoreContextImpl<ActivityType>.sendOnBackgroundThread(
action: ActivityType.() -> Unit
) {
val sendLatch = CountDownLatch(1)
executeOnBackgroundThread {
send {
action()
sendLatch.countDown()
}
}
sendLatch.throwOnTimeout()
}
}
3 changes: 3 additions & 0 deletions settings.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -17,3 +17,6 @@ include(
":samples:stopwatch-compose",
":samples:todoapp"
)
include(
":test-utils:android"
)
1 change: 1 addition & 0 deletions test-utils/android/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
/build
20 changes: 20 additions & 0 deletions test-utils/android/build.gradle.kts
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
plugins {
id("com.android.library")
id("kotlin-android")
id("kotlin-parcelize")
}

android {
namespace = "com.instacart.testutils.android"
}

dependencies {
implementation(project(":formula-rxjava3"))
implementation(project(":formula-android"))

implementation(libs.kotlin)
implementation(libs.androidx.appcompat)
implementation(libs.androidx.test.core.ktx)
implementation(libs.lifecycle.extensions)
implementation(libs.robolectric)
}
14 changes: 14 additions & 0 deletions test-utils/android/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android">

<application>
<activity
android:name="com.instacart.testutils.android.TestFragmentActivity"
android:launchMode="standard"
android:exported="false" />

<activity
android:name="com.instacart.testutils.android.TestActivity"
android:launchMode="standard"
android:exported="false" />
</application>
</manifest>
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
package com.instacart.testutils.android

import android.app.Activity

class TestActivity : Activity()
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
package com.instacart.testutils.android

import android.app.Activity
import android.os.Looper
import androidx.test.core.app.ActivityScenario
import org.robolectric.Shadows
import java.util.concurrent.CountDownLatch
import java.util.concurrent.Executors
import java.util.concurrent.TimeUnit

fun <A: Activity> ActivityScenario<A>.activity(): A {
return get { this }
}

fun <A: Activity, T> ActivityScenario<A>.get(select: A.() -> T): T {
val list: MutableList<T> = mutableListOf()
onActivity {
list.add(it.select())
}
return list.first()
}

fun CountDownLatch.throwOnTimeout() {
if (!await(100, TimeUnit.MILLISECONDS)) {
throw IllegalStateException("timeout")
}
}

fun executeOnBackgroundThread(action: () -> Unit) {
val initLatch = CountDownLatch(1)
Executors.newSingleThreadExecutor().execute {
action()
initLatch.countDown()
}
initLatch.throwOnTimeout()
Shadows.shadowOf(Looper.getMainLooper()).idle()
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
package com.instacart.testutils.android

import androidx.fragment.app.FragmentActivity

class TestFragmentActivity : FragmentActivity()
Loading