0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

【Android】ProcessLifecycleOwnerとDefaultLifecycleObserverのUNITテストを書く

Posted at

この記事では、ProcessLifecycleOwnerとDefaultLifecycleObserverを使用したクラスの実装と、Robolectricを使用したユニットテストの書き方について説明します。

目次

AndroidのProcessLifecycleOwnerの概要

ProcessLifecycleOwnerはAndroidアプリケーション全体のライフサイクルを表す、シングルトンのLifecycleOwnerです。通常のActivityやFragmentのライフサイクルとは異なり、ProcessLifecycleOwnerはアプリ全体が前面に表示されているか(フォアグラウンド)、背面に隠れているか(バックグラウンド)を追跡します。

主なイベントは以下のように処理されます:

  • ON_START: アプリがフォアグラウンドに移行したとき
  • ON_STOP: アプリがバックグラウンドに移行したとき

重要な点として、ProcessLifecycleOwnerは内部的に約700msの遅延を使用して、新しいアクティビティが作成されていないことを確認してからON_STOPイベントを発火させます。

AppLifecycleObserverの実装

まず、ProcessLifecycleOwnerを使用してアプリのライフサイクルを監視するクラスを実装してみます。

import androidx.lifecycle.DefaultLifecycleObserver
import androidx.lifecycle.LifecycleOwner
import androidx.lifecycle.ProcessLifecycleOwner
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asStateFlow
import javax.inject.Inject
import javax.inject.Singleton

/**
 * アプリケーションのライフサイクル状態を監視し、アプリがフォアグラウンドにあるか
 * バックグラウンドにあるかを追跡するクラス
 */
@Singleton
class AppLifecycleObserver @Inject constructor() : DefaultLifecycleObserver {
    
    // アプリのフォアグラウンド状態を表すFlow
    private val _appInForeground = MutableStateFlow(false)
    val appInForeground: StateFlow<Boolean> = _appInForeground.asStateFlow()
    
    // 最後にフォアグラウンドに入った時間
    private var lastForegroundTime: Long = 0
    
    // アプリが起動されてからの合計フォアグラウンド時間
    private var totalForegroundTime: Long = 0
    
    init {
        // ProcessLifecycleOwnerに自身をオブザーバーとして登録
        ProcessLifecycleOwner.get().lifecycle.addObserver(this)
    }
    
    /**
     * アプリがフォアグラウンドに入ったときに呼ばれる
     */
    override fun onStart(owner: LifecycleOwner) {
        _appInForeground.value = true
        lastForegroundTime = System.currentTimeMillis()
    }
    
    /**
     * アプリがバックグラウンドに移行したときに呼ばれる
     */
    override fun onStop(owner: LifecycleOwner) {
        _appInForeground.value = false
        val currentTime = System.currentTimeMillis()
        val sessionDuration = currentTime - lastForegroundTime
        totalForegroundTime += sessionDuration
    }
    
    /**
     * 現在のセッション時間を取得する
     * アプリがフォアグラウンドにある場合は現在の時間から計算し、
     * バックグラウンドにある場合は0を返す
     */
    fun getCurrentSessionDurationInSeconds(): Long {
        return if (_appInForeground.value) {
            (System.currentTimeMillis() - lastForegroundTime) / 1000
        } else {
            0
        }
    }
    
    /**
     * アプリケーションの合計フォアグラウンド時間を秒単位で取得する
     */
    fun getTotalForegroundTimeInSeconds(): Long {
        return totalForegroundTime / 1000 + getCurrentSessionDurationInSeconds()
    }
}

このクラスは、アプリがフォアグラウンドにあるかどうかの状態を追跡し、アプリの使用時間を計測する機能を提供します。

Applicationクラスでの使用例は以下の通りです:

import android.app.Application
import dagger.hilt.android.HiltAndroidApp
import javax.inject.Inject

@HiltAndroidApp
class MyApplication : Application() {
    
    @Inject
    lateinit var appLifecycleObserver: AppLifecycleObserver
    
    override fun onCreate() {
        super.onCreate()
        // AppLifecycleObserverの初期化は、コンストラクタのProcessLifecycleOwner.get()で行われます
    }
}

Robolectricを使用したテスト

Robolectricは、JVM上でAndroidテストを実行できるフレームワークで、エミュレータやデバイスなしでテストを高速に実行できます。
今回はRobolectricでProcessLifecycleOwnerをテストする方法を紹介します。

import androidx.arch.core.executor.testing.InstantTaskExecutorRule
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.testing.TestLifecycleOwner
import androidx.test.core.app.ApplicationProvider
import androidx.test.ext.junit.runners.AndroidJUnit4
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.test.runTest
import org.junit.Before
import org.junit.Rule
import org.junit.Test
import org.junit.runner.RunWith
import org.robolectric.RobolectricTestRunner
import org.robolectric.Shadows
import org.robolectric.annotation.Config
import org.robolectric.shadows.ShadowLooper
import kotlin.test.assertEquals
import kotlin.test.assertFalse
import kotlin.test.assertTrue

@ExperimentalCoroutinesApi
@RunWith(RobolectricTestRunner::class)
@Config(sdk = [28]) // 対象のSDKバージョンを指定
class AppLifecycleObserverTest {
    
    // JUnitルール: テスト中にアーキテクチャコンポーネントの操作を同期的に実行
    @get:Rule
    val instantTaskExecutorRule = InstantTaskExecutorRule()
    
    // テスト対象
    private lateinit var appLifecycleObserver: AppLifecycleObserver
    
    // TestLifecycleOwner - ライフサイクルイベントを制御するために使用
    private lateinit var testLifecycleOwner: TestLifecycleOwner
    
    @Before
    fun setup() {
        // TestLifecycleOwnerの初期化
        testLifecycleOwner = TestLifecycleOwner()
        
        // AppLifecycleObserverの初期化
        appLifecycleObserver = AppLifecycleObserver()
        
        // 対象クラスをTestLifecycleOwnerに登録
        testLifecycleOwner.lifecycle.addObserver(appLifecycleObserver)
    }
    
    @Test
    fun `アプリ起動時にフォアグラウンド状態がtrueになること`() = runTest {
        // アプリ起動時のライフサイクルイベントをシミュレート
        testLifecycleOwner.currentState = Lifecycle.State.STARTED
        
        // フォアグラウンド状態を確認
        val isInForeground = appLifecycleObserver.appInForeground.first()
        assertTrue(isInForeground)
    }
    
    @Test
    fun `アプリがバックグラウンドに移行したときにフォアグラウンド状態がfalseになること`() = runTest {
        // 最初にアプリをフォアグラウンドにする
        testLifecycleOwner.currentState = Lifecycle.State.STARTED
        
        // バックグラウンドへの移行をシミュレート
        testLifecycleOwner.currentState = Lifecycle.State.CREATED
        
        // バックグラウンド状態を確認
        val isInForeground = appLifecycleObserver.appInForeground.first()
        assertFalse(isInForeground)
    }
    
    @Test
    fun `バックグラウンド時のセッション時間が0になること`() = runTest {
        // アプリをバックグラウンドにする
        testLifecycleOwner.currentState = Lifecycle.State.STARTED
        testLifecycleOwner.currentState = Lifecycle.State.CREATED
        
        // バックグラウンド時のセッション時間をチェック
        assertEquals(0, appLifecycleObserver.getCurrentSessionDurationInSeconds())
    }
    
    @Test
    fun `フォアグラウンド時にセッション時間が増加すること`() {
        // アプリをフォアグラウンドにする
        testLifecycleOwner.currentState = Lifecycle.State.STARTED
        
        // 最初のセッション時間を記録
        val initialDuration = appLifecycleObserver.getCurrentSessionDurationInSeconds()
        
        // 時間の経過をシミュレート(Robolectricの機能を使用)
        ShadowLooper.idleMainLooper(1000) // 1秒進める
        
        // セッション時間が増加しているか確認
        val newDuration = appLifecycleObserver.getCurrentSessionDurationInSeconds()
        assertTrue(newDuration > initialDuration)
    }
    
    @Test
    fun `合計フォアグラウンド時間がセッション間で累積されること`() {
        // 1回目のフォアグラウンドセッション
        testLifecycleOwner.currentState = Lifecycle.State.STARTED
        ShadowLooper.idleMainLooper(1000) // 1秒進める
        testLifecycleOwner.currentState = Lifecycle.State.CREATED
        
        // 1回目のセッション後の合計時間を記録
        val totalAfterFirstSession = appLifecycleObserver.getTotalForegroundTimeInSeconds()
        
        // 2回目のフォアグラウンドセッション
        testLifecycleOwner.currentState = Lifecycle.State.STARTED
        ShadowLooper.idleMainLooper(1000) // 1秒進める
        testLifecycleOwner.currentState = Lifecycle.State.CREATED
        
        // 2回目のセッション後の合計時間を確認
        val totalAfterSecondSession = appLifecycleObserver.getTotalForegroundTimeInSeconds()
        
        // 合計時間が累積しているか確認
        assertTrue(totalAfterSecondSession > totalAfterFirstSession)
    }
    
    @Test
    fun `複数のライフサイクル遷移で状態が正しく更新されること`() = runTest {
        // 初期状態チェック - バックグラウンド
        assertFalse(appLifecycleObserver.appInForeground.first())
        
        // フォアグラウンドへ
        testLifecycleOwner.currentState = Lifecycle.State.STARTED
        assertTrue(appLifecycleObserver.appInForeground.first())
        
        // バックグラウンドへ
        testLifecycleOwner.currentState = Lifecycle.State.CREATED
        assertFalse(appLifecycleObserver.appInForeground.first())
        
        // 再びフォアグラウンドへ
        testLifecycleOwner.currentState = Lifecycle.State.STARTED
        assertTrue(appLifecycleObserver.appInForeground.first())
    }
    
    @Test
    fun `ProcessLifecycleOwnerの遅延動作を再現するテスト`() = runTest {
        // この例では、ProcessLifecycleOwnerの700ms遅延をRobolectricでシミュレート
        
        // アプリをフォアグラウンドにする
        testLifecycleOwner.currentState = Lifecycle.State.STARTED
        assertTrue(appLifecycleObserver.appInForeground.first())
        
        // バックグラウンドへの移行
        testLifecycleOwner.currentState = Lifecycle.State.CREATED
        
        // 時間の経過をシミュレート(ProcessLifecycleOwnerの遅延よりも短い時間)
        ShadowLooper.idleMainLooper(500) // 500ms進める
        
        // バックグラウンド状態を確認
        assertFalse(appLifecycleObserver.appInForeground.first())
        
        // 実際のProcessLifecycleOwnerでは、この時点でまだON_STOPは発火していないが
        // テスト環境ではすでに状態が変更されている
    }
}

モックを使用した別のテストアプローチ

TestLifecycleOwnerが使用できない場合や、より詳細な検証が必要な場合は、LifecycleRegistryをモックする方法もあります。

import androidx.lifecycle.Lifecycle
import androidx.lifecycle.LifecycleOwner
import androidx.lifecycle.LifecycleRegistry
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.test.runTest
import org.junit.Before
import org.junit.Test
import org.junit.runner.RunWith
import org.mockito.Mock
import org.mockito.Mockito.`when`
import org.mockito.Mockito.mock
import org.mockito.MockitoJUnitRunner
import org.mockito.Mockito.verify
import org.mockito.Spy
import kotlin.test.assertFalse
import kotlin.test.assertTrue

@ExperimentalCoroutinesApi
@RunWith(MockitoJUnitRunner::class)
class AppLifecycleObserverWithMockTest {
    
    // テスト対象
    @Spy
    private lateinit var appLifecycleObserver: AppLifecycleObserver
    
    // LifecycleOwnerをモック
    @Mock
    private lateinit var lifecycleOwner: LifecycleOwner
    
    // LifecycleRegistryをモック
    private lateinit var lifecycleRegistry: LifecycleRegistry
    
    @Before
    fun setup() {
        // LifecycleRegistryの作成
        lifecycleRegistry = LifecycleRegistry(mock(LifecycleOwner::class.java))
        
        // モックの設定
        `when`(lifecycleOwner.lifecycle).thenReturn(lifecycleRegistry)
        
        // AppLifecycleObserverの初期化
        appLifecycleObserver = AppLifecycleObserver()
        
        // オブザーバーをLifecycleRegistryに登録
        lifecycleRegistry.addObserver(appLifecycleObserver)
    }
    
    @Test
    fun `onStartが呼ばれるとフォアグラウンド状態がtrueになること`() = runTest {
        // onStartメソッドを直接呼び出す
        appLifecycleObserver.onStart(lifecycleOwner)
        
        // フォアグラウンド状態を確認
        assertTrue(appLifecycleObserver.appInForeground.first())
    }
    
    @Test
    fun `onStopが呼ばれるとフォアグラウンド状態がfalseになること`() = runTest {
        // まずフォアグラウンドに設定
        appLifecycleObserver.onStart(lifecycleOwner)
        
        // onStopメソッドを呼び出す
        appLifecycleObserver.onStop(lifecycleOwner)
        
        // バックグラウンド状態を確認
        assertFalse(appLifecycleObserver.appInForeground.first())
    }
    
    @Test
    fun `ライフサイクルイベントで適切なオブザメソッドが呼ばれること`() {
        // ライフサイクルイベントをシミュレート
        lifecycleRegistry.handleLifecycleEvent(Lifecycle.Event.ON_START)
        
        // onStartが呼ばれたか確認
        verify(appLifecycleObserver).onStart(lifecycleRegistry.observerUnderObservation)
        
        // ライフサイクルイベントをシミュレート
        lifecycleRegistry.handleLifecycleEvent(Lifecycle.Event.ON_STOP)
        
        // onStopが呼ばれたか確認
        verify(appLifecycleObserver).onStop(lifecycleRegistry.observerUnderObservation)
    }
}

この方法では、LifecycleRegistryhandleLifecycleEventメソッドを使用して、特定のライフサイクルイベントを発火させ、オブザーバーの対応するメソッドが呼び出されるかを検証します。

必要な依存関係

これらのクラスとテストを実行するために必要なGradle依存関係は以下の通りです:

// app/build.gradle.kts
dependencies {
    // ライフサイクルコンポーネント
    implementation("androidx.lifecycle:lifecycle-process:2.6.2")
    implementation("androidx.lifecycle:lifecycle-runtime-ktx:2.6.2")
    
    // Kotlinコルーチン
    implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.7.3")
    implementation("org.jetbrains.kotlinx:kotlinx-coroutines-android:1.7.3")
    
    // Dagger Hilt(必要な場合)
    implementation("com.google.dagger:hilt-android:2.48")
    kapt("com.google.dagger:hilt-android-compiler:2.48")
    
    // テスト依存関係
    testImplementation("junit:junit:4.13.2")
    testImplementation("org.jetbrains.kotlin:kotlin-test:1.9.10")
    testImplementation("org.mockito:mockito-core:5.5.0")
    testImplementation("org.mockito:mockito-inline:5.5.0")
    testImplementation("org.jetbrains.kotlinx:kotlinx-coroutines-test:1.7.3")
    testImplementation("androidx.arch.core:core-testing:2.2.0")
    testImplementation("androidx.lifecycle:lifecycle-runtime-testing:2.6.2") // TestLifecycleOwnerを使用するため
}

特に重要なのは、androidx.lifecycle:lifecycle-runtime-testing依存関係で、これによってTestLifecycleOwnerクラスが使用可能になります。


0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?