はじめに
エンジニアであればUNITテストやUIテストを書いていると思います。
ActivityやFragmentは参考になるespresso等のテスト記事を多く見かけます。
しかし、ウィジェット(RemoteViews)のテスト方法がネット上を検索してもあまりなかったのでメモがてら残しておきます。
仕様
まず、ウィジェットの仕様を考えます。
テストがメインなのでシンプルな仕様にします。
・Viewはボタンとテキスト
・ボタンを押すと1づつカウントアップ
・テキストの初期表示は0
・最大は12で最大を超えると0に戻る
・3の倍数で文字色が赤になる
ウィジェットの実装
まずはウィジェットを実装して動かしてみましょう。
ウィジェットなのでAppWidgetProviderクラスが必要です。
import android.appwidget.AppWidgetManager
import android.appwidget.AppWidgetProvider
import android.content.ComponentName
import android.content.Context
import android.content.Intent
class TestAppWidgetProvider : AppWidgetProvider() {
private lateinit var testRemoteViews: TestRemoteViews
override fun onUpdate(
context: Context,
appWidgetManager: AppWidgetManager,
appWidgetIds: IntArray
) {
for (appWidgetId in appWidgetIds) {
// SharedPreferencesから保存しているカウントを読み出す
val clickCount = getClickCount(context)
// ウィジェットを更新
getTestRemoteViews(context).updateAppWidget(
appWidgetManager,
appWidgetId,
clickCount.toString(),
isSpecialCount(clickCount)
)
}
}
override fun onReceive(context: Context?, intent: Intent?) {
super.onReceive(context, intent)
if (context !is Context || intent !is Intent) return
when (intent.action) {
COUNT_UP -> {
// 1足した値を取得
val clickCount = getAddCount(context)
putClickCount(context, clickCount)
// TextViewにカウント値を適用
val views = getTestRemoteViews(context)
views.setCounterTextViewText(clickCount.toString(), isSpecialCount(clickCount))
// ウィジェットを更新
val myWidget = ComponentName(context, TestAppWidgetProvider::class.java)
val manager = AppWidgetManager.getInstance(context)
manager.updateAppWidget(myWidget, views)
}
}
}
private fun getTestRemoteViews(context: Context): TestRemoteViews {
if (!::testRemoteViews.isInitialized) testRemoteViews = TestRemoteViews(context)
return testRemoteViews
}
/**
* カウントアップ後のカウント値を読み込む.
*/
private fun getClickCount(context: Context): Int {
val dataStore = context.getSharedPreferences("test_widget", Context.MODE_PRIVATE)
return dataStore.getInt("click_count", DEFAULT_COUNT)
}
/**
* カウントアップ後のカウント値を書き込む.
*/
private fun putClickCount(context: Context, clickCount: Int) {
val dataStore = context.getSharedPreferences("test_widget", Context.MODE_PRIVATE)
dataStore.edit().putInt("click_count", clickCount).apply()
}
private fun getAddCount(context: Context): Int {
var clickCount = getClickCount(context)
clickCount++
return if (clickCount > MAX_COUNT) {
DEFAULT_COUNT
} else {
clickCount
}
}
/**
* 3の倍数か.
*/
private fun isSpecialCount(clickCount: Int): Boolean {
if (clickCount <= 0) return false
return (clickCount % 3 == 0)
}
companion object {
const val COUNT_UP = "intent.TestAppWidgetProvider.CountUp"
const val DEFAULT_COUNT = 0
const val MAX_COUNT = 12
}
}
RemoteViewsで表示を更新する実装にしました。
import android.app.PendingIntent
import android.appwidget.AppWidgetManager
import android.content.Context
import android.content.Intent
import android.widget.RemoteViews
import androidx.core.content.ContextCompat
class TestRemoteViews(val context: Context) :
RemoteViews(context.packageName, R.layout.test_app_widget) {
fun updateAppWidget(
appWidgetManager: AppWidgetManager,
appWidgetId: Int,
clickCount: String,
isSpecialColor: Boolean
) {
//TextViewにカウント値を適用
setCounterTextViewText(clickCount, isSpecialColor)
//Button押下通知用のPendingIntentを作成しに登録
val countIntent = Intent(context, TestAppWidgetProvider::class.java).apply {
action = TestAppWidgetProvider.COUNT_UP
}
val countPendingIntent =
PendingIntent.getBroadcast(context, 0, countIntent, PendingIntent.FLAG_UPDATE_CURRENT)
setOnClickPendingIntent(R.id.add_button, countPendingIntent)
// ウィジェットを更新
appWidgetManager.updateAppWidget(appWidgetId, this)
}
fun setCounterTextViewText(clickCount: String, isSpecialColor: Boolean) {
setTextViewText(R.id.counter_text, clickCount)
if (isSpecialColor) {
setTextColor(
R.id.counter_text,
ContextCompat.getColor(context, android.R.color.holo_red_light)
)
} else {
setTextColor(R.id.counter_text, ContextCompat.getColor(context, android.R.color.black))
}
}
}
レイアウトも作っておきます。
previewImageは今回は仮なのでアプリアイコンをセットします。
<?xml version="1.0" encoding="utf-8"?>
<appwidget-provider xmlns:android="http://schemas.android.com/apk/res/android"
android:initialLayout="@layout/test_app_widget"
android:minWidth="40dp"
android:minHeight="40dp"
android:previewImage="@mipmap/ic_launcher"
android:resizeMode="horizontal|vertical"
android:updatePeriodMillis="86400000" />
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="@android:color/holo_green_light"
android:gravity="center"
android:orientation="vertical"
android:padding="@dimen/widget_margin">
<TextView
android:id="@+id/counter_text"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="0"
android:textColor="@android:color/black"
android:textStyle="bold" />
<Button
android:id="@+id/add_button"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="+" />
</LinearLayout>
AndroidManifestにも忘れずに記載します。
<receiver android:name="TestAppWidgetProvider" >
<intent-filter>
<action android:name="android.appwidget.action.APPWIDGET_UPDATE" />
</intent-filter>
<meta-data android:name="android.appwidget.provider"
android:resource="@xml/test_appwidget_info" />
</receiver>
ウィジェットの動作確認
ビルドしてエミュレータにインストールします。
↓のようなウィジェットができました。
ボタンをクリックするとカウントアップして3の倍数で文字が赤くなります。
UIテストの準備
ここからが本番です。
テストの為にgradleに定義を追加します。
android {
// ↓を忘れずに追加
testOptions {
unitTests {
includeAndroidResources = true
}
}
}
dependencies {
// ↓を追加(最新のバージョンにしてください)
testImplementation 'junit:junit:4.13'
testImplementation 'androidx.test:core:1.3.0'
testImplementation 'org.robolectric:robolectric:4.3.1'
}
テスト用のApplicationクラスを作ります。
クラスの作成場所はtestパッケージ配下です。
import android.app.Application
class TestApplication : Application() {
override fun onCreate() {
super.onCreate()
// ここにテスト用に必要な処理が書ける
// FirebaseApp.initializeApp(this)
// といった必要な初期処理が書けるので便利
}
}
robolectric.propertiesというファイルを作ります。
ファイルを app/src/test/resources ディレクトリに置おきます。
sdk=28
application=com.example.widgetapplication.TestApplication
UIテストを書く
準備ができたのでテストクラスを作成しテストを書きます。
import android.content.Context
import android.widget.FrameLayout
import android.widget.TextView
import androidx.core.content.ContextCompat
import androidx.test.core.app.ApplicationProvider
import org.junit.Assert
import org.junit.Before
import org.junit.Test
import org.junit.runner.RunWith
import org.robolectric.RobolectricTestRunner
@RunWith(RobolectricTestRunner::class)
class TestRemoteViewsTest {
private lateinit var context: Context
@Before
fun init() {
// これでContextが取得できる
context = ApplicationProvider.getApplicationContext()
}
@Test
fun 初期値のテスト() {
// この方法でRemoteViewが取得できる
val testRemoteViews = TestRemoteViews(context)
val widgetView = testRemoteViews.apply(context, FrameLayout(context))
// Viewの情報を取得
// テキスト情報取得
val counterTextView = widgetView.findViewById<TextView>(R.id.counter_text)
// 文字取得
val counterTextString = counterTextView.text.toString()
// 文字色取得
val currentTextColor = counterTextView.currentTextColor
// テスト
// レイアウトのテスト レイアウトを出し分けている場合には有効かも
Assert.assertEquals(R.layout.test_app_widget, testRemoteViews.layoutId)
// 初期表示の確認 文字と色
Assert.assertEquals("0", counterTextString)
Assert.assertEquals(ContextCompat.getColor(context, android.R.color.black), currentTextColor)
}
@Test
fun 倍数のテスト() {
// RemoteViewに値をセット
val testRemoteViews = TestRemoteViews(context)
val count = "3"
val isSpecialColor = true
testRemoteViews.setCounterTextViewText(count, isSpecialColor)
val widgetView = testRemoteViews.apply(context, FrameLayout(context))
// Viewの情報を取得
// テキスト情報取得
val counterTextView = widgetView.findViewById<TextView>(R.id.counter_text)
// 文字取得
val counterTextString = counterTextView.text.toString()
// 文字色取得
val currentTextColor = counterTextView.currentTextColor
// テスト
// 表示の確認 レイアウト、文字、色
Assert.assertEquals(R.layout.test_app_widget, testRemoteViews.layoutId)
Assert.assertEquals("3", counterTextString)
Assert.assertEquals(ContextCompat.getColor(context, android.R.color.holo_red_light), currentTextColor)
}
}
テストを実行するとパスすることを確認できました。
最後に
今回は簡単なテストしか書いていませんが応用も可能かと思います。
テストは大事なのでしっかりバグがないアプリを作っていきましょう。