ラベル CI/CD の投稿を表示しています。 すべての投稿を表示
ラベル CI/CD の投稿を表示しています。 すべての投稿を表示

2024年7月6日土曜日

Android テストコードの実装について解説

どうも。どっことです。今回は、テストコードの実装について解説したいと思います。

テストコード

テストコードは実機やエミュレータに開発段階のアプリをインストールせずとも、プログラムとしてテストを実行できるようにするものです。テストコードを実装せずとも、世の中に公開するアプリを開発することができるので、より難しい機能の実装や複雑なドメインロジックをプログラムに落とし込むなどのスキルを優先して獲得するというロードマップも考えとしてはあると思います。

ただ、昨今のCI/CDの重要性の高まりを鑑みると、テストコードを実装できることもエンジニアとして必要なスキルとして需要が上がってきているように見受けられます。そしてそんな需要が高まっている一方で、テストコードを実装できるエンジニアはまだまだ多くありません。ぜひテストコードを実装できるようになり、より広い知見をもつエンジニアへと一歩を進めましょう。

説明に入る前に(テストコードの種類)

Androidの開発においては、大きく2種類のテストコードがあります。

ユニットテスト

開発PC上で実行されるテストコードです。Androidの機能に依存しないビジネスロジックやアプリケーションロジックに対するテストコードを実装する際のものとなります。開発PC上で実行されるため、後述のインストルメントテストよりも高速に実行・実施ができます。

インストルメントテスト

実機やエミュレーター上で実行されるテストコードです。こちらは、Androidの機能に依存するUIまわりのテストや、外部機能などとの連携に対するテストコードとなります。より現実に近い環境でのテストとなるため、ユーザーストーリーを意識したテストに向いています。一方で、インストールなどのステップがビルド時に含まれるため、実行時間が比較的遅めになります。

今回は、ユニットテストの実装方法について解説します。

ユニットテストを実装する

ユニットテストの実装方法について、以下の流れで説明します。

  • ライブラリの準備
  • 実装
  • その他
    • Robolecticの紹介

ライブラリの準備

ユニットテストのテストコードを実装するためには以下をbuild.gradleに追加が必要です。プロジェクトを新規作成する際にAndroid Studioが自動で追加している場合がありますので、追加が必要か確認してください。

dependencies {
    testImplementation("junit:junit:4.13.2")
}

実装

それでは実装していきましょう。

テスト対象

テスト対象は以下のクラス・メソッドとします。

class Sample {
   fun test(input: Int?): Boolean {
       if (input == null) {
           throw IllegalArgumentException()
       }
       return input > 0
    }
}

テスト観点

テスト対象となるメソッドは、以下の観点でテストコードを実装することができます。

  • trueが返される確認テスト
  • falseが返される確認テスト
  • 例外が投げられるテスト

上をベースに確認するテストコードを追加していきましょう。

テストクラスを追加する

テストを実装するため、テストクラスを実装します。テスト対象のクラスで option + Enter からCreate Testを選択すればテストクラスを作成できます。SampleTestという名前にすると、testディレクトリに以下のようなクラスが作成できると思います。

class SampleTest {
}

trueが返される確認テストを実装する

まずはtrueが返されることのテストです。

@Test
fun testTrue() {
    val sample = Sample()
    val value = sample.test(1)
    Assert.assertEquals(value, true)
}

以降の実装も同様ですが、テストメソッドを実装し@Testアノテーションを付与します。

そして、実際にテストしたい内容を実装します。今回は、入力値に1以上を指定した場合はtrueが返却されるので、それを確認するためのテストを実装します。

falseが返される確認テストを実装する

次にfalseが返されることのテストです。

@Test
fun testFalse() {
    val sample = Sample()
    val value = sample.test(0)
    Assert.assertEquals(value, false)
}

同様に、falseが返却されるケースを実装します。入力値に0以下を指定した場合はfalseが返却されるので、それを確認するためのテストを実装します。

例外が投げられるテストを実装する

そして、例外が投げられることのテストです。

@Test(expected = IllegalArgumentException::class)
fun testException() {
    val sample = Sample()
    sample.test(null)
    Assert.fail()
}

例外が発生するテストは@Testアノテーションのパラメータexpectedを指定します。そのパラメータには発生する例外のクラスを指定します。

また、例外が発生しなかった場合がテストがNGとなるように、例外がthrowされる直後のタイミングでAssert.fail()を実装しておきます。

これにて、該当クラスメソッドのテストが実装できました。あとはテストを実行すれば期待した結果になるかを確認する処理が実行されます。

その他

ここまでテストコードの実装について解説しましたが、実は1点問題があります。ユニットテストは開発PC上で実行されますが、その都合により、AndroidSDKに含まれる機能を利用することができません。これが結構ネックになり、例えばよくある実装の一つであるFragmentのインスタンス生成メソッドは、Fragmentやそれに渡すBundleがAndroidSDKに含まれるものなので、テストコードを実装することができません。

ではどうするかというと、一つはインストルメントテストで実装することです。実機やエミュレーター上でテストすることで、テストコードを実行することができます。

もう一つはRobolectricを利用することです。RobolectricはユニットテストでもAndroidSDKに含まれる機能を利用することができます。

Robolectricの紹介

今回は説明を省略しますが、公式サイトを紹介します。またRobolectricの利用方法についても、後日解説を予定しています。

まとめ

今回はテストコード、特にユニットテストの実装方法について解説しました。テストコードやその実装の重要性は今後も高いものと予想されるので、今後もこれをテーマとした投稿をしていこうと考えていますので、よろしくお願いします。

参考

2024年5月5日日曜日

Android mockito-kotlinを使ったテストコードの書き方を紹介

どうも。どっことです。今回は、mockito-kotlinライブラリを使ったKotlinでの単体テストの書き方を紹介します。

mockito-kotlinを使ったモック化

CI/CDの重要性が注目されて久しいですが、単体テストをはじめとしたテストコードを実装できるエンジニアはまだまだ多くありません。またテストコードは実装経験あるけど、いざ実装しようとした時に「あれってどうやるんだっけ」となるような、テストコードの実装がまだ手に馴染んでいない人も多いかと思います。

今回は単体テストのテストコードを実装する上で必須のテクニックとなるモック化について紹介したいと思います。

モック化とは

他の機能の影響によりテストが不安定な状態となることを防ぐため、あらかじめ固定の値を返したり、特定の振る舞いをさせるように指定しておくことです。

実装方法

以下の順に解説していきます。

  • 使用するライブラリ(mockito-kotlinライブラリの紹介)
  • 導入方法
    • 実装方法

    使用するライブラリ

    オブジェクトをモック化するにあたり、JavaやKotlinではさまざまなモック化ライブラリが公開されています。今回はその中のひとつであるmockito-kotlinを使った導入・実装方法を解説していきます。

    mockito-kotlin

    もともとJavaのモックフレームワークとして利用されているMockitoライブラリを、Kotlinでも簡単に利用できるような機能を具備しているライブラリがmockito-kotlinです。公式ページを参考欄に載せておくので、詳しい情報をお探しの方はそちらを参照してください。

    導入方法

    いつものごとく、build.gradleのdependenciesに追記します。以下を追加してください。

    dependencies {
      testImplementation "org.mockito.kotlin:mockito-kotlin:5.3.1"
    }

    実装方法

    それでは、モック化する実装方法について解説します。

    モック化の実装①戻り値の固定化

    戻り値を固定化する実装は以下です。 mockでモックするクラス、onでモックするメソッドを指定します。そしてdoReturnで戻り値を指定します。

    val mock = mock<Sample> {
        on { sample() } doReturn "sample"
    }

    モック化の実装②例外

    メソッドが呼ばれた時に特定の例外を投げるようなモックの実装は以下です。doThrowで例外を指定します。

    val mock = mock<Sample> {
        on { sample() } doThrow IllegalStateException()
    }

    モック化の実装③振る舞いの指定

    メソッドが呼ばれた時に特定の振る舞いをさせるモックの実装は以下です。doAnswerで具体的な振る舞いを指定します。

    val mock = mock<Sample> {
        on { sample() } doAnswer {
            // do anything.
        }
    }

    まとめ

    今回は、テストコードを実装する上で必要不可欠なテクニックであるモック化をmockito-kotlinライブラリを使って実装する方法について解説しました。他の機能に依存しない疎結合なテストコードを実装するためにモック化は重要なテクニックとなるので、手に馴染むまで何度も実装しましょう。

    参考

    2023年8月1日火曜日

    CI/CD Bitriseでaabを生成する

    前回の続きです。今回はリリースファイルを作るための各種設定を備忘録として記載したいと思います。

    リリースファイル(aab)を生成するための設定

    とはいえ、基本は以下の手順通りに設定すればOK。日本語ページはなかなか残念なことになっているので、英語ページをベースに作業したほうが結果的にストレスが少なくて済む。

    Generating and deploying Android app bundles

    ただし、少しだけハマりポイントがあるので注意が必要。ハマったポイントは以下。

    gradlewのパス

    唯一納得いかなかったハマりポイント。gradlewのパスがデフォルトのままだとビルドが通らない。

    上記ページの手順で追加した「Gradle Runner」のステップ内にあるInput variables>gradlew file pathの項目に以下を設定する。

    ./gradlew

    最小構成のプロジェクトなら、これだけ手直しすればビルドが通ってaabができるはず。

    (追記)最新の設定だと、「Gradle Runner」を追加せずともaabファイルを生成できる模様...何のために試行錯誤したのか。。。

    google-services.jsonの準備

    google-services.jsonはFirebaseの機能を使うためにアプリに組み込む設定ファイル。秘匿情報が含まれるため、原則Gitには登録しない。しかし、Gitに登録しないとBitriseでビルドする時にこの設定ファイルが無いせいでビルドに失敗する。

    この矛盾を解消するために、Bitriseに用意されているテキスト生成のステップを利用する。ざっくり書くと、以下。

    1. ファイルの中身をまるっとコピーする。
    2. コピーしたテキストをBitriseの定数として保存。
      • 定数の名前は適当でOK。例えば直感的にGOOGLE_SERVICES_JSONとか。
    3. テキストファイル生成のステップ(Generate Text File)を追加。
      • 定数として定義したテキストを使って、google-services.jsonファイルとして生成する。

    「テキストファイルを生成する」ステップが用意されているのが、直感的で非常にわかりやすい。

    センシティブな情報の設定

    おそらくAndroidアプリ開発者ならほとんどの人がハマるポイント。build.gradleBuildConfigFieldを定義する際、ダブルクォーテーションを定義に含めないと、生成されるBuildConfigでいい感じに定義できない問題。これがBitriseでも(というより環境変数として定義する場合でも)同様に発生する。

    何も考えないならbuild.gradle内で

    buildConfigField "String", "AD_UNIT_ID", "\"広告ID\""

    って形で定義すれば問題ない。

    しかし、これを(センシティブな情報だから、外部に設定+それを参照するような形とするために)環境変数などに設定しそれを参照する形で

    buildConfigField "String", "AD_UNIT_ID", System.getEnv(定義したキー)

    って書くと残念ながらビルドに失敗する。設定値側(環境変数)にダブルクォーテーションを含めて定義するか、参照時に文字テンプレートを使うことで解消できる。

    AndroidManifest.xmlも参照する必要が出てくるはずなので、文字テンプレートを使った方が無難。つまり以下。

    buildConfigField "String", "AD_UNIT_ID", "${System.getEnv(定義したキー)}"

    参考

    2023年7月31日月曜日

    CI/CD 各種サービスを比較し使うものを選定する

    この前の続き。CI/CD環境の構築に向けて、良さそうなサービスを探す。

    CI/CDと聞いて真っ先に思いつくのはJenkinsだが、オンデマンドに構築できるほど私のプライベート環境は充実していないので却下。外部サービスで検討する。

    調査・知っている範囲で、以下のサービスをピックアップした。

    • Github Action
    • Circle CI
    • TravisCI
    • Bitrise

    で、結論としてBitriseで進めることにした。

    理由としては

    • Bitriseはモバイルアプリ(特にAndroid)のインテグレーション(単体テストなど)、デプロイ(apkやaabの生成、GooglePlayStoreへのアップロード)が初期状態でほぼ構築済み。ユーザーは必要なパラメーターを入力するだけでほぼ実現できる。

    Bitrise以外のサービスについては

    • GitHubActionはリポジトリをpublicにしたいという要件を満たせない。署名キーや広告IDは秘匿にできるにせよ、それにより生成されたバイナリが表に丸見えになりうる。やり方次第かもだが、バイナリを生成するワークフローを表に見せたくない…。
    • CircleCIはfastlaneの導入が必要になる。iOSアプリ開発者だと、コマンド一つでipaを生成するために導入したりなど比較的馴染みのあるツールだが、AndroidでCI/CDするためだけにわざわざ導入したいと思わない。
    • 残るTravisCIがBitriseの対抗馬となったが、導入の容易さから結果としてBitriseに軍配があがることとなった。

    ただし注意事項として、Bitriseは1アカウントにつき処理できるジョブやタスクの回数?リソースの使用量?に制限がある。無料アカウントでは、毎月「300クレジット」という単位でサービスを利用できるが、これを越えると次のクレジット支給まで使えなくなる様子。
    正直どんなもん?というところだが、(構築後日談として)1回のワークフロー(ビルドをするためのセットアップからビルド完了までの一連の流れ)を処理するのに大体10クレジット程度を消費しているため、30日間ではおよそ30回しか利用できない、ということになる。
    とはいえ「プライベートで回している開発フローで、複数のプロジェクトを加味する必要があるとはいえ1ヶ月に30回もリリースビルド→デプロイすることあるか…?」と考えると、これが制限となってしまうことは稀になりそうだ。

    というわけで今回はCI/CD環境を構築するためのサービスの選定とその理由について投稿した。次回はBitriseを利用したAndroidアプリのデプロイ環境の構築について投稿予定だ。

    2023年7月15日土曜日

    CI/CD 最近考えていること リリース手順が面倒くさい

    毎回毎回、

    署名キー探して、
    設定値を入力してビルドして
    aabファイル生成して
    プレイストアにアップロードして

    ってフローが面倒になった。どうにかならないものか。

    少なくても、署名キー探す~aabファイル生成するまでを自動化できることは知っているけど、例えばGitHubActionでやるなら生成したaabファイルや署名キーを秘匿にしないといけない都合でリポジトリも秘匿にしないといけないし、でもそれを秘匿にするのは正直あまりうれしくない(できるだけパブリックにしておきたい)。どうにかならないものか。

    って考えたときに、「ソースを管理する領域(GitHub)とaabファイル生成をする領域を別にすればいいんじゃん。そうすればソースはパブリックのままでいいし、署名キーやら生成したaabファイルやらも秘匿の状態を保てる。とても幸せやん。」と気づいた。

    上記を整理すると以下のフローとなる。

    1. ローカル環境で開発
    2. GitHubに変更をプッシュ
    3. GitHubはGitHubActionのジョブを回す
      • テストを回したり
      • Readme更新したり
      • CI/CDサービスに登録してあるジョブのトリガーを引いたり ★1
    4. 前項のトリガーを受けて、CI/CDサービスはaabファイルを生成する。
      1. GitHubからソースコードをクローンしてくる。
      2. aabファイルを生成し、成果物として管理。
    5. CI/CDサービスはプレイストアにaabファイルをアップロードする。 ★2
      1. どこまでできるか次第だが、リリースノートなども合わせてアップロードできればなお嬉しい。
    6. CI/CDサービスはすべての処理が完了したことを通知する。
    7. 私はそれを確認して、特に問題なければリリース。何かあれば適宜修正。

    素敵だ。上記フローを実現するために必要な調査は★1,2の内容。

    • ★1:登録したジョブを実行するトリガーを外部から引くことはできる?(CI/CDのサービス次第?)
    • ★2:aabファイルをプレイストアにアップロードするクチ(=IF)は用意されている?どこまでの情報をアップロードできる?

    まあ、「そんなのありません」ってことはないんだろうけどね。特に★1については「サービスとしてはあるけど、有料会員様限定のサービスなんすよ」かもしれないし。

    というわけで、考えていることでした。調査した結果とか、更新があればまた改めて投稿します。

    移行予定

    どうも。どっことです。 タイトルの通りですが、諸事情により GitHubPage に移行予定です。 https://mkt120.github.io/ この備忘録に記載の内容を転記しつつ、今後はこちらのページを更新していく予定です。