そもそも自動UIテストとは?
アプリ開発を行う上で欠かすことができないのが、『テスト』です。
・アイコンやテキストはちゃんと表示されているか?
・ボタンをタップすると期待通りの動作をするか?
などなど、様々な要素や状況をテストし、アプリの品質に問題がないことを確かめる必要があります。
この時、ごく小規模なもの(ちょっとしたサンプルや学習用のものなど)であれば、ビルドして実機やエミュレータで手動的にテストを行うことも比較的容易です。
実際に目で見て確かめる。実際にボタンをタップしてみる。
このように手動(物理)的にテストを行って、アプリに問題がないことを確認することも、もちろん大事です。
しかし、これが大規模なアプリになるとどうでしょうか?
アプリに含まれる全てのテキストや画像、ボタン、その他のパーツなどを、いちいち手動で確認していくのは大変ですよね。
最終的には人間の目や手を使って確認する必要があるにしても、ある程度テストを自動化して効率的に進めていくようにしないと、テストだけで膨大なリソースが割かれてしまいます。
そこで大活躍するのが、Jetpack Composeに用意された自動UI(レイアウト)テストです。
自動テストを利用することで、いちいちアプリをビルドして人間の目で確認せずとも、『その画面に表示されているべきテキスト』や、『ボタンが押された時に起こるべき動作(結果)』を、論理的に確認することができます。
テストに合格すれば、少なくとも論理的には問題なく開発できていることが保証されますし、逆に不合格になったとしたら、そこに問題があることをハッキリさせられます。
自動テストは義務的なものではなく、飛ばそうと思えば飛ばせるものではありますが、アプリ開発を効率よく進めるための強力な機能なのです。
ただ、このように思う方もいるかもしれません。
「でもそれって企業が作るような大規模なアプリの話でしょ?私は個人開発で小規模なアプリしか開発しないから関係なさそうだなぁ。」
…正直なところ、私自身がそう思っていました。笑
ですが開発経験を積んでいくうちに、私は開発リソースが限られる個人開発者こそ、この自動テスト機能をうまく利用したほうが良いと思うようになりました。
最初は小規模なアプリだったとしても、改良を加えるうちに、なんやかんや複雑化・大規模化していく…ということはよくありますし、そうなるとメンテナンスも大変になってきます。
開発者が手動で全てのテストを行っている場合、改良を重ねるごとにテストがどんどん大変になっていくため、これ以上改良を重ねたくないと考えてしまうかもしれません。
一方で、自動テストをうまく活用していれば、改良による必要なテストの増加はあまり負担ではなくなり、改良を加えたいという意欲も保ちやすいと言えます。
ということで、よほど小規模で、かつ将来的にも全く改良・拡張する予定のないアプリでない限り、自動テストによる開発の効率化、品質の維持・向上効果を利用しない手はありません。
とは言え、自動テストをマスターするためには学ばなければならないことが多く、最初から全てを把握しようとするのはあまり現実的ではありません。
そこで次の章では、まず自動テストの超基本と言えるところだけに的を絞ってみたいと思います。
基本に的を絞ることで、『自動テストって思ったより簡単で便利そうだな』と思って頂ければ幸いです!
POINT!
- ・自動UIテストを利用することで、アプリ開発に必要なテストを効率化できる!
- ・開発リソースが限られる個人開発者にとっても、自動テストの導入メリットは大きい!
- ・自動テストの仕組みはやや複雑なので、まずは基本を理解することから始めよう!
自動UIテストの流れ
この章では、自動UIテストを行うまでの流れと、シンプルな自動UIテストの例をご紹介していきます。
1: 依存関係の確認
Gradle Scripts > build.gradle.kts(Module :app) を開くと、現在の依存関係の状況を確認できます。
デフォルト(Empty Activityから新しくプロジェクトを作成した場合)では、次のようになっています。(2023年10月現在)
このうち、testImplementationや、androidTestImplementation、debugImprementationが、テストに関連する部分です。
行いたい自動テストの種別などによって、追加しなければならないライブラリは異なりますが、今回はデフォルトのままでOKです。
2: テスト記述用ファイルの作成
既存のファイルに上書きしても良いのですが、テストの用途や目的別にテスト用のファイルを作成して管理しておいた方がわかりやすいので、新しくファイルを作成しておきましょう。
ここで、テストファイルを作成する場所が重要になります。
実はテストにも色々な種類があり、単に関数のロジックが正しいかどうかを確認するようなテスト(単体・ユニットテスト)や、特定の画面に特定のテキストやボタンが正しく配置されているかどうかや、ボタンタップなどユーザーアクションによって起こる動作などを確認するテスト(UI・統合テスト)などがあります。
Android Studioでは、テストの種類によって管理されるパッケージが異なるので、ユニットテスト用のファイルをUIテスト用のパッケージに作成したり、UIテスト用のファイルをユニットテスト用のパッケージに作成したりすると、テストがうまくいかないことがあります。
ここでは、簡単なUIテストを行いたいので、UIテスト用のパッケージであるcom.example.sample(androidTest)パッケージの方にテスト用のKotlinファイルを作成します。
com.example.sample(androidTest)上で右クリック ➔ New ➔ Kotlin Class/File を選択し、Classが選択された状態で、ファイルに任意の名前をつけて作成します。(MyTestなど)
3: テストコードの記述
ファイルの作成ができたら、実際にシンプルなテストを記述して実行してみましょう。
New Project ➔ Empty Activity を選択してプロジェクトを開始したばかりの状態である場合、アプリにはGreetingと命名されたComposableがあり、そこでは『Hello $name!』という文字列が表示されるようになっているはずです。(nameはGreeting Composableに引数として渡される文字列)
@Composable
fun Greeting(
name: String, modifier: Modifier = Modifier
) {
Text(
text = "Hello $name!",
modifier = modifier
)
}
せっかくなのでこのGreeting Composableを利用してテストを作成してみましょう。
Greeting Composableに”Android”という文字列を渡すと、”Hello Android!”という文字列がアプリの画面に表示されるはずですから、それをUIテストで確認します。
ということでさっそくテストを実行するためのコードを記述していこうと思うのですが、その前に一つ注意点があります。
テストを行うには様々な設定・セットアップが必要であり、それを指定するためのコードも当然必要です。
ただし、その仕組みを根本的なところから深く・正しく知ろうとすると、関連するライブラリについて細かく知る必要が出てくるので、膨大な時間と労力がかかってしまいます。
なので、最初は『UIテストではこういうふうにコードを書けば良いんだな』といったような感覚的な理解に留めておいたり、表面的な部分をざっくり理解するぐらいでOKとしたりなど、どこかで線引きをしておくことをオススメします。
その点に気をつけつつ、テスト用のシンプルなコードを書いていきましょう。
まず、テストを行う上では何らかの基準(ルール)や環境設定が必要です。
実際の商品のテストでも、寒い環境でテストを行うのか(耐寒性能テスト)、衝撃を加える環境でテストを行うのか(耐衝撃性テスト)など、色々なルール・環境の元で行われますよね。
ということで、まずはルールを定義していきます。
一般的なUIテストにおいては、次のようにルールを定義します。
@get:Rule
val composeTestRule = createAndroidComposeRule<ComponentActivity>()
@get: Rule というのは、UIテストを行うのに必要なルールを定義するためのアノテーションです。
そして、次の val composeTestRule = createAndroidComposeRule<ComponentActivity>()
で、必要なセットアップ・環境作成が定義されます。
これで必要なルールの定義と環境のセットアップができたので、次に『どのようなテストを行うか』といった、テスト本体の部分を記述していきます。
まず、『これはテストを行う関数である』ということを明示的に示すため、関数の前に@Testアノテーションをつけます。
なお、今回のテストでは期待するテキストが正しく表示されるかどうかを確認することが目的なので、関数名は `checkText` などにしておくとわかりやすいかと思います。
@Test
fun checkText() { }
準備が完了したところで、テスト関数の本体を記述していきましょう。
まず、ルールやテスト環境を定義しただけでは意味がありませんので、それらを適用させるために、定義しておいたcomposeTestRuleをsetContent
に適用させた上で、テストを行いたいテーマ・コンポーザブルをセッティングします。
@Test
fun checkText() {
composeTestRule.setContent {
SampleTheme {
Greeting(name = "Android")
}
}
}
そして次がテスト関数の肝となる部分です。
すなわち、『何をテスト(確認)したいか』に関するコードを記述します。
今回は、Greeting Composableに『Hello Android!』というテキストが表示されていることを確認したいわけですから、それをテストできるロジックを組み立てます。
表示テキストを確認するためのテストを行うコードは次のようになります。
@Test
fun checkText() {
composeTestRule.setContent {
SampleTheme {
Greeting(name = "Android")
}
}
composeTestRule.onNodeWithText("Hello Android!").assertIsDisplayed()
}
まず、composeTestRule
でどのルール・環境で行うテストであるのかを示し、次にonNodeWithText()
で表示されているべきテキストを検索しています。
そして、assertIsDisplayed()
でそのテキストが表示されているかどうかを判定し、表示されていれば合格となります。
このテストを実行するには、定義したテスト関数(checkText)の左側にある緑色の▶ボタンをクリックします。
なお、UIテストは実機もしくはエミュレータ上で実行されます。
なので、実機やエミュレータが使用できない状況(例:実機との接続が正しくできていない、エミュレータがインストールされていないなど)ではテストができないので注意してください。
Greeting Composableには『Hello Andorid!』というテキストを持つテキストノードが存在し、画面上に表示されている状態であるため、このテストを実行すると無事に合格します。
しかし、下のように『hello Andoriod!』など、異なる文字列を指定してテストを実行すると失敗します。
composeTestRule.onNodeWithText("hello Android!").assertIsDisplayed()
このように、大文字・小文字といった違いでも、デフォルトでは異なるものとして判別する設定になっています。
ただ、大文字と小文字を区別してほしくない場合も当然ありますよね。
onNodeWithText()
にはいくつかのオプション設定があり、ignoreCase = true
とすることで大文字・小文字を異なるものとして区別せずにテスト判定を行うことができます。
composeTestRule.onNodeWithText(
text = "hello Android!",
ignoreCase = true
).assertIsDisplayed()
今回ご紹介したテストは、『表示されているべきテキストが表示されているかどうかを確認する』という、ごくシンプルなものですが、まずはこういった基本的なテストから導入すると良いと思います。
onNodeWithText()
では純粋なテキストだけでなく、ボタンテキストなどのテキストも検索できるため、「この画面にはホームに戻るための『HOME』ボタンテキストが表示されていなければならないが、それをテストでも確認しておきたい」といったような場面でも活用できます。
とは言え、テキストが表示されているかどうかのテストだけでは少し物足りませんよね。
そこで次の章ではもう一歩踏み込んだテストとして、『ボタンが押されたことにより、表示されるテキストが変わったことを確認するテスト』の例をご紹介します。
POINT!
- ・自動UIテストを導入する際は、依存関係とテストファイルの作成場所に注意!
- ・テストコードには普段は使わないアノテーションやメソッドが出てくるが、最初は『そういうものがある』ぐらいの理解でOK!
- ・onNodeWithText()のignoreCaseをtrueに設定することで、大文字と小文字の区別を無視できる!
ユーザーアクションをテストに加える
通常、アプリはテキストを表示するだけでなく、ユーザーによる様々な操作を受け付けます。
そのユーザーによる操作の代表的な例が、ボタンタップ(クリック)でしょう。
ボタンをタップすると別のページに飛んだり、データを保存したり削除したり…様々な操作を行うことができますよね。
テストではこういった一連の流れ(ユーザーアクション ➔ 返される結果)を確認したいという場合も多いです。
Jetpack Composeを使った自動UIテストでは、ボタンタップなどのようなユーザーアクションを含めたテストを実施することもできます。
そこで、この章では例として、『ボタンをタップすると表示されているテキストが変わる』というサンプルを用意して、それを自動テストで確認してみようと思います。
まず、前章で利用したGreeting Composableをちょこっと改造しましょう。
テキストの表示に引数を利用するのではなく、状態変更が可能な状態(MutableState)で文字列を定義しておき、それを利用するようにします。
そして、Button Composableを新しく用意して、タップするとMutableStateで定義しておいたテキストの文字列が変わるように onClick
パラメータで設定します。
サンプルコードは次のようになります。
@Composable
fun Greeting(modifier: Modifier = Modifier) {
var name by remember {
mutableStateOf("Android")
}
Column(
verticalArrangement = Arrangement.Center,
horizontalAlignment = Alignment.CenterHorizontally,
modifier = modifier
) {
Text(text = "Hello $name!")
Button(onClick = { name = "Jetpack Compose" } ) {
Text(text = "Change Text")
}
}
}
要は、初期状態では『Hello Android!』という文字列が表示されるようにしておき、ボタンをタップすると『Hello Jetpack Compose!』に変わるようにしたというわけですね。
実際、ボタンをタップすると下のように表示が変わります。
さて、これで準備が整いました。あとはこの一連の流れを自動テストに組み込んでみましょう。
ルールやテスト環境の定義、セッティング方法に変更はありません。変わるのは基本的なセッティングを行った後の部分です。
まず、ボタンがタップされる前のテキスト(Hello Android!)が表示されていることを確認するコードを記述します。
// initial text
composeTestRule.onNodeWithText(
text = "hello android!",
ignoreCase = true
).assertIsDisplayed()
ここまでは前章と全く同じですね。
そして次に、ユーザーアクション(ここではボタンタップ)を追加します。
ボタンには『Change Text』というテキストを指定しているので、それを利用してボタンノードを検索し、performClick()
でクリック(タップ)動作を実行します。
// button click
composeTestRule.onNodeWithText(
text = "change text",
ignoreCase = true
).performClick()
最後に、表示されるテキストがちゃんと変わっているかどうかを確認します。
ボタンが押されると『Hello Jetpack Compose!』に変わるはずなので、この文字列を指定して結果と比較します。
// text after button clicked
composeTestRule.onNodeWithText(
text = "hello jetpack compose!",
ignoreCase = true
).assertIsDisplayed()
テストを実行すると、ちゃんと合格することが確認できます。
全体的なテストコードは下のようになりますので、参考にしてください。
import androidx.activity.ComponentActivity
import androidx.compose.ui.test.assertIsDisplayed
import androidx.compose.ui.test.junit4.createAndroidComposeRule
import androidx.compose.ui.test.onNodeWithText
import androidx.compose.ui.test.performClick
import com.example.sample.ui.theme.SampleTheme
import org.junit.Rule
import org.junit.Test
class MyTest {
@get:Rule
val composeTestRule = createAndroidComposeRule<ComponentActivity>()
@Test
fun checkText() {
composeTestRule.setContent {
SampleTheme {
Greeting()
}
}
// initial text
composeTestRule.onNodeWithText(
text = "hello android!",
ignoreCase = true
).assertIsDisplayed()
// button click
composeTestRule.onNodeWithText(
text = "change text",
ignoreCase = true
).performClick()
// text after button clicked
composeTestRule.onNodeWithText(
text = "hello jetpack compose!",
ignoreCase = true
).assertIsDisplayed()
}
}
アプリに設置されているボタンが1つや2つぐらいなら、実際に確認した方が早い場合もありますが、ボタンが増えれば増えるほど、手動で確認するのが困難になり、確認に要する時間も増していきます。
ですが今回紹介した自動テストをうまく使うことで、こういった確認を効率的に行うことができるようになります。
大規模なアプリ開発ではもちろん、個人開発による比較的小規模なアプリであっても、自動テストの導入によって受けられる恩恵は大きいです。
最初は『手動で確認した方が楽なんじゃないの?』と思ってしまうこともあるかもしれませんが、自動テストに慣れれば慣れるほど、その便利さと有り難みを感じることができるかと思います。
POINT!
- ・自動UIテストにはボタンタップなどのユーザーアクションを含めることができる!
- ・ボタンタップは、対象のノードを指定して.performClick()を実行すればOK!
- ・大規模なアプリ開発現場に限らず、開発リソースが限られる個人開発でこそ自動テストを活用しよう!