月: 2023年2月

[Android]Activityの結果を受け取るUIテスト

Activity内のボタンを押すとActivityが終了するというテストを書く場合

<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout
    xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context="jp.co.example.app.activity.MyActivity">

    <com.google.android.material.appbar.AppBarLayout
        android:id="@+id/appbar"
        android:layout_width="0dp"
        android:layout_height="wrap_content"
        android:theme="@style/AppTheme.AppBarOverlay"
        app:layout_constraintTop_toTopOf="parent"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintEnd_toEndOf="parent">

        <androidx.appcompat.widget.Toolbar
            android:id="@+id/toolbar"
            android:layout_width="match_parent"
            android:layout_height="?android:attr/actionBarSize"
            android:background="?attr/colorPrimaryDark"
            app:navigationIcon="@drawable/ic_toolbar_close"/>

    </com.google.android.material.appbar.AppBarLayout>

    <androidx.fragment.app.FragmentContainerView
        android:id="@+id/navHostFragment"
        android:name="androidx.navigation.fragment.NavHostFragment"
        android:layout_width="0dp"
        android:layout_height="0dp"
        app:defaultNavHost="true"
        app:layout_constraintTop_toBottomOf="@id/appbar"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintBottom_toBottomOf="parent"/>

</androidx.constraintlayout.widget.ConstraintLayout>
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout
    xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    android:layout_width="match_parent"
    android:layout_height="match_parent">

    <com.google.android.material.button.MaterialButton
        android:id="@+id/finishButton"
        android:layout_width="0dp"
        android:layout_height="0dp"
        app:layout_constraintTop_toTopOf="parent"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintBottom_toBottomOf="parent"/>

</androidx.constraintlayout.widget.ConstraintLayout>
<?xml version="1.0" encoding="utf-8"?>
<navigation
    xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    android:id="@+id/my_navigation"
    app:startDestination="@id/myFragment">

    <fragment
        android:id="@+id/myFragment"
        android:name="jp.co.example.app.fragment.MyFragment">

        <argument
            android:name="isFirstFragment"
            app:argType="boolean"
            app:nullable="false"/>

    </fragment>

</navigation>
@AndroidEntryPoint
class MyActivity : AppCompatActivity() {
  private lateinit var binding: ActivityMyBinding // activity_my.xmlのレイアウトファイルがあり、ViewBindingで使う

  override fun onCreate(savedInstanceState: Bundle?) {
    super.onCreate(savedInstanceState)

    binding = ActivityMyBinding.inflate(layoutInflater)
    setContentView(binding.root)
    
    findPrimaryNavFragment()?.findNavController()?.setGraph(
      R.navigation.my_navigation,
      MyFragmentArgs(true).toBundle()
    )

    binding.toolbar.title = "タイトル"
    setSupportActionBar(binding.toolbar)
    binding.toolbar.setNavigationOnClickListener { finish() } // ツールバーの左のアイコンを押すとMyActivityが終了する
  }
  
  companion object {
    fun createIntent(context: Context): Intent = Intent(context, MyActivity::class.java)
  }
} 
@AndroidEntryPoint
class MyFragment : Fragment() {
  private var _binding: FragmentMyBinding? = null
  private val binding get() = _binding!!
  private val args: MyFragmentArgs by navArgs()

  override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View {
    _binding = FragmentMyBinding.inflate(inflater)
    return binding.root
  }

  override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
    super.onViewCreated(view, savedInstanceState)
    binding.finishButton.setOnClickListener {
      requireActivity().setResult(
        Activity.RESULT_OK,
        Intent().apply { putExtra("boolean_result", args.isFirstFragment) }
      )
      requireActivity().finish()
    }
  }
  
  override fun onDestroyView() {
    super.onDestroyView()
    _binding = null
  }
}     

これらのコードで画面上のボタンを押すとActivity.RESULT_OKのコードでboolean_resultがtrueで返されるテストコードと画面左上のボタンを押して何も返却されないことを確認するテストコードは下記になります。

abstract class BaseHiltActivityTest {

    @Suppress("LeakingThis")
    @get:Rule
    var hiltRule = HiltAndroidRule(this)

    @CallSuper
    @Before
    open fun setup() {
        hiltRule.inject()
    }

    fun sleepUntilDisplayUi(time: Long = 2000L) {
        TimeUnit.MILLISECONDS.sleep(time)
    }
}
@RunWith(AndroidJUnit4::class)
@HiltAndroidTest
class MyActivityTest : BaseHiltActivityTest() {
  private fun launchActivity(): ActivityScenario<MyActivity> {
    val intent = MyActivity.createIntent(ApplicationProvider.getApplicationContext())
    val scenario = ActivityScenario.launchActivityForResult<MyActivity>(intent)
    sleepUntilDisplayUi()
    return scenario
  }

  @Test
  fun launchActivity_returnResult() {
    val scenario = launchActivity()
    onView(withId(R.id.finishButton)).perform(click())
    assertEquals(scenario.result.resultCode, Activity.RESULT_OK)
    assertEquals(scenario.result.resultData.getBooleanExtra("boolean_result", false), true)
  }

  @Test
  fun launchActivity_returnEmpty() {
    val scenario = launchActivity()
    // NavigationアイコンのDescriptionにはR.string.abc_action_bar_up_descriptionが設定されている
    onView(withContentDescription(R.string.abc_action_bar_up_description)).perform(click())
    assertEquals(scenario.result.resultCode, Activity.RESULT_CANCELED)
    assertNull(scenario.result.resultData)
  }
}

ポイントとしてはval scenario = ActivityScenario.launchActivityForResult<MyActivity>(intent)で受け取ったscenarioからscenario.result.resultCodeでActivity.RESULT_XXXが取得でき、scenario.result.resultDataで終了時に渡したIntentデータが取得できる点です。

ActivityScenario.launchActivity<MyActivity>(intent)だと結果を取得できないので注意です。


AndroidStudioから直接Crashlyticsを見る

https://medium.com/androiddevelopers/see-crashlytics-issue-reports-directly-in-android-studio-with-app-quality-insights-db0ff27454f0

AndroidStudio ElectricEelがリリースされました。新しくAppQualityInsightsタブが追加されたので指示ど通りにログインします。このときFirebaseにログインしているアカウントでログインしてください。それ以外のアカウントだと見れません。

AndroidStudio上でCrashlyticsの内容が見れます。

過去のクラッシュ、スタックトレースとコードジャンプ、発生した端末の比率、発生したAndroidのバージョンの比率、期間での絞り込み、アプリバージョンでの絞り込み、FatalとNon-Fatalの切り替え、などができます。

ブラウザでクラッシュを確認してコードを探す手間が省けて一気にコードジャンプができるので開発がラクになります。